返回首页

如何对 bundle 体积进行监控和分析?

问题解析

Bundle 体积直接影响页面加载性能和用户体验。面试官通过此题考察候选人对性能优化的关注程度,以及是否具备使用专业工具分析和监控 bundle 体积的能力。

核心概念

Bundle 体积监控和分析的核心目标:

  1. 可视化分析:直观了解模块组成和体积分布
  2. 体积监控:防止 bundle 体积无限增长
  3. 优化指导:找出大体积模块和重复依赖
  4. CI 集成:自动化体积检测和告警

详细解答

1. webpack-bundle-analyzer

可视化分析工具,生成模块组成树状图,直观展示各模块体积。

安装

npm install webpack-bundle-analyzer --save-dev

基本配置

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      // 分析模式
      analyzerMode: 'server',  // server | static | disabled

      // 服务器主机
      analyzerHost: 'localhost',

      // 服务器端口
      analyzerPort: 8888,

      // 自动打开浏览器
      openAnalyzer: true,

      // 生成报告文件名(static 模式)
      reportFilename: 'report.html',

      // 默认尺寸显示
      defaultSizes: 'parsed',  // stat | parsed | gzip

      // 是否展示完全填充的模块
      generateStatsFile: false,

      // stats 文件名
      statsFilename: 'stats.json',

      // 日志级别
      logLevel: 'info'
    })
  ]
};

分析模式

// 1. Server 模式(默认)- 启动 HTTP 服务器
new BundleAnalyzerPlugin({
  analyzerMode: 'server',
  analyzerPort: 8888,
  openAnalyzer: true
});

// 2. Static 模式 - 生成 HTML 文件
new BundleAnalyzerPlugin({
  analyzerMode: 'static',
  reportFilename: 'bundle-report.html',
  openAnalyzer: false  // CI 环境不自动打开
});

// 3. Disabled 模式 - 仅生成 stats 文件
new BundleAnalyzerPlugin({
  analyzerMode: 'disabled',
  generateStatsFile: true,
  statsFilename: 'stats.json'
});

解读分析报告

┌─────────────────────────────────────────────────────────────┐
│  Bundle Analyzer Report                                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │  vendor.js (1.2 MB)
│                                                             │
│  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                                    │  main.js (450 KB)
│                                                             │
│  ▓▓▓▓▓▓▓▓▓▓▓▓                                               │  lodash (280 KB)
│                                                             │
│  ▓▓▓▓▓▓▓▓▓▓                                                 │  moment (180 KB)
│                                                             │
│  ▓▓▓▓▓▓                                                      │  react-dom (120 KB)
│                                                             │
│  ▓▓▓▓                                                        │  @mui/material (85 KB)
│                                                             │
└─────────────────────────────────────────────────────────────┘

颜色说明:
- 蓝色: 正常模块
- 红色: 体积过大模块(> 250 KB)
- 黄色: 重复依赖

2. bundlesize

自动化资源体积监控工具,可在 CI 中集成,超出阈值时阻止合并。

安装

npm install bundlesize --save-dev

配置 package.json

{
  "bundlesize": [
    {
      "path": "./dist/*.js",
      "maxSize": "500 kB",
      "compression": "gzip"
    },
    {
      "path": "./dist/vendor.*.js",
      "maxSize": "300 kB",
      "compression": "gzip"
    },
    {
      "path": "./dist/main.*.js",
      "maxSize": "100 kB",
      "compression": "gzip"
    },
    {
      "path": "./dist/*.css",
      "maxSize": "50 kB",
      "compression": "gzip"
    }
  ]
}

独立配置文件(bundlesize.config.json)

{
  "files": [
    {
      "path": "./dist/app-*.js",
      "maxSize": "200 kB",
      "compression": "gzip"
    },
    {
      "path": "./dist/chunk-*.js",
      "maxSize": "100 kB",
      "compression": "gzip"
    }
  ]
}

CI 集成

# .github/workflows/bundle-size.yml
name: Bundle Size

on: [pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: '16'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Check bundle size
        run: npx bundlesize

输出示例

 PASS  ./dist/main.js: 45.2 kB < 100 kB (gzip)
 PASS  ./dist/vendor.js: 245.8 kB < 300 kB (gzip)
 FAIL  ./dist/app.js: 520 kB > 500 kB (gzip)

  Bundle size exceeds limit!
  Consider code splitting or lazy loading.

3. 其他分析工具

size-plugin

const SizePlugin = require('size-plugin');

module.exports = {
  plugins: [
    new SizePlugin({
      pattern: '**/*.{js,css}',
      filename: 'size-plugin.json',
      writeFile: true,
      stripHash: (filename) => filename.replace(/\.[a-f0-9]{8,}\./g, '.')
    })
  ]
};

webpack-bundle-tracker

const BundleTracker = require('webpack-bundle-tracker');

module.exports = {
  plugins: [
    new BundleTracker({
      filename: './webpack-stats.json',
      includeChunks: ['main', 'vendor']
    })
  ]
};

duplicate-package-checker-webpack-plugin

const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');

module.exports = {
  plugins: [
    new DuplicatePackageCheckerPlugin({
      verbose: true,
      emitError: false,
      exclude: (instance) => instance.name === 'lodash'
    })
  ]
};

深入理解

体积分析维度

// 1. Stat 大小 - 原始代码大小
const statSize = fs.readFileSync(file).length;

// 2. Parsed 大小 - 编译后大小
const parsedSize = bundleContent.length;

// 3. Gzip 大小 - 压缩后大小
const gzipSize = zlib.gzipSync(bundleContent).length;

// Bundle Analyzer 展示的是 parsed 大小
// 但 gzip 大小更接近网络传输体积

自定义体积分析插件

class BundleSizeAnalyzerPlugin {
  constructor(options = {}) {
    this.options = {
      maxSize: options.maxSize || 500 * 1024,  // 500 KB
      warnSize: options.warnSize || 250 * 1024, // 250 KB
      ...options
    };
  }

  apply(compiler) {
    compiler.hooks.emit.tap('BundleSizeAnalyzer', (compilation) => {
      const assets = compilation.assets;
      const report = {
        timestamp: new Date().toISOString(),
        files: [],
        totalSize: 0,
        warnings: [],
        errors: []
      };

      for (const filename in assets) {
        if (filename.endsWith('.js')) {
          const asset = assets[filename];
          const size = asset.size();
          const gzipSize = this.getGzipSize(asset.source());

          report.totalSize += size;

          const fileReport = {
            name: filename,
            size: this.formatSize(size),
            gzipSize: this.formatSize(gzipSize),
            rawSize: size
          };

          report.files.push(fileReport);

          // 检查阈值
          if (size > this.options.maxSize) {
            report.errors.push(`${filename} 超出限制: ${this.formatSize(size)} > ${this.formatSize(this.options.maxSize)}`);
          } else if (size > this.options.warnSize) {
            report.warnings.push(`${filename} 体积较大: ${this.formatSize(size)}`);
          }
        }
      }

      // 排序
      report.files.sort((a, b) => b.rawSize - a.rawSize);

      // 输出报告
      this.printReport(report);

      // 写入文件
      this.writeReport(report);
    });
  }

  getGzipSize(source) {
    const zlib = require('zlib');
    return zlib.gzipSync(source).length;
  }

  formatSize(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
    return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
  }

  printReport(report) {
    console.log('\n=== Bundle Size Report ===\n');
    console.log(`总大小: ${this.formatSize(report.totalSize)}\n`);

    console.log('文件列表:');
    report.files.forEach(file => {
      console.log(`  ${file.name}: ${file.size} (gzip: ${file.gzipSize})`);
    });

    if (report.warnings.length) {
      console.log('\n警告:');
      report.warnings.forEach(w => console.log(`  ⚠️  ${w}`));
    }

    if (report.errors.length) {
      console.log('\n错误:');
      report.errors.forEach(e => console.log(`  ❌ ${e}`));
    }

    console.log('\n========================\n');
  }

  writeReport(report) {
    const fs = require('fs');
    fs.writeFileSync(
      'bundle-size-report.json',
      JSON.stringify(report, null, 2)
    );
  }
}

module.exports = BundleSizeAnalyzerPlugin;

最佳实践

1. 完整的体积监控配置

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');
const SizePlugin = require('size-plugin');

const isAnalyze = process.env.ANALYZE === 'true';
const isCI = process.env.CI === 'true';

module.exports = {
  plugins: [
    // 体积变化监控
    new SizePlugin({
      pattern: '**/*.{js,css}',
      filename: 'size-plugin.json',
      publish: isCI  // CI 环境上报
    }),

    // 重复依赖检查
    new DuplicatePackageCheckerPlugin({
      verbose: true,
      emitError: isCI  // CI 环境报错
    }),

    // 可视化分析(仅在 ANALYZE=true 时启用)
    ...(isAnalyze ? [
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        reportFilename: 'bundle-report.html',
        openAnalyzer: !isCI,
        generateStatsFile: true,
        statsFilename: 'stats.json'
      })
    ] : [])
  ]
};

2. package.json 脚本

{
  "scripts": {
    "build": "webpack --mode production",
    "build:analyze": "ANALYZE=true npm run build",
    "size": "bundlesize",
    "size:check": "npm run build && npm run size"
  }
}

3. CI/CD 集成

# .github/workflows/size.yml
name: Size Check

on:
  pull_request:
    branches: [main]

jobs:
  size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v2
        with:
          node-version: '16'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Check bundle size
        run: npx bundlesize

      - name: Upload analysis
        uses: actions/upload-artifact@v2
        with:
          name: bundle-analysis
          path: |
            bundle-report.html
            stats.json
            size-plugin.json

4. 体积优化检查清单

// 优化前分析
const optimizationChecklist = {
  // 1. 代码分割
  codeSplitting: {
    vendor: '是否分离第三方库?',
    routes: '是否按路由分割?',
    async: '是否使用动态导入?'
  },

  // 2. 依赖优化
  dependencies: {
    lodash: '是否使用 lodash-es 替代 lodash?',
    moment: '是否移除 moment 本地化文件?',
    duplicates: '是否存在重复依赖?'
  },

  // 3. 构建优化
  build: {
    treeShaking: 'Tree Shaking 是否生效?',
    minification: '代码是否压缩?',
    gzip: '是否启用 Gzip/Brotli 压缩?'
  },

  // 4. 资源优化
  assets: {
    images: '图片是否压缩?',
    fonts: '字体文件是否按需加载?',
    css: 'CSS 是否提取并压缩?'
  }
};

5. 体积预算配置

// webpack.config.js
module.exports = {
  performance: {
    // 启用性能提示
    hints: 'warning',  // 'error' | 'warning' | false

    // 入口点大小限制
    maxEntrypointSize: 250000,  // 250 KB

    // 资源大小限制
    maxAssetSize: 250000,  // 250 KB

    // 只检查 JS 文件
    assetFilter: function(assetFilename) {
      return assetFilename.endsWith('.js');
    }
  }
};

面试要点

  1. 主要工具

    • webpack-bundle-analyzer:可视化分析模块组成
    • bundlesize:自动化体积监控和阈值检查
  2. 分析维度

    • Stat:原始代码大小
    • Parsed:编译后大小
    • Gzip:压缩后大小(最接近网络传输)
  3. CI 集成

    • PR 时自动检查 bundle 体积
    • 超出阈值阻止合并
    • 生成可视化报告
  4. 优化方向

    • 代码分割(Code Splitting)
    • 移除重复依赖
    • Tree Shaking
    • 按需加载
  5. 体积预算

    • 设置 entrypoint 和 asset 大小限制
    • 集成到 CI 流程
    • 定期审查和优化
  6. 常见问题

    • 重复依赖(如多个 lodash 版本)
    • 未使用的代码
    • 大型第三方库未按需引入
    • 缺少代码分割