前端包体与拆包

让 Agent 用 bundle analyzer、source map 与路由级 code splitting 控制首屏与缓存策略;读懂 treemap 与预算是缩小根因的第一步。

SKILL 写明 gzip、brotli 与未压缩原始体积的预算阈值,以及第三方库替代方案评审流程;避免将仅管理端使用的重型依赖打进公共 chunk。

动态 import 与 prefetch 策略需与导航路径和用户权限一致;对国际化与主题等大资源采用按需加载与长期缓存指纹。

CI 中接入体积回归门禁时,Agent 应解释增长来自新增依赖还是业务代码,并给出 tree-shaking 或拆包的具体改法。

  • vendor 与 app 分离,稳定 hash 利于浏览器缓存。
  • 同步脚本阻塞渲染的路径单独标红。
  • 图片与字体走 CDN 与合适格式(AVIF、woff2)。

包体分析主流程(build → treemap → 预算)

  [ 生产构建:与线上同配置(minify、define、target)]
        │
        ▼
  ┌─────────────┐     stats / visualize:webpack-bundle-analyzer、Rollup Visualizer、source-map-explorer
  │  产物 + map  │──── 输出 treemap:矩形面积 ≈ 该模块在图中的贡献(工具口径见文档)
  └─────────────┘
        │
        ▼
  ┌─────────────┐     对照 SKILL:首包 / 路由 chunk / vendor 上限;标 gzip 或 brotli 主口径
  │  预算表      │──── 超出项:重复依赖、误打全量 polyfill、sideEffects 误判、动态边界错误
  └─────────────┘
        │
        ▼
  ┌─────────────┐     size-limit / bundlewatch / 自定义阈值;PR 附 treemap 截图或报告链接
  │  CI 门禁     │──── 回归说明:新依赖版本、import 路径、公共 chunk 合并策略变更
  └─────────────┘

先固定「报告口径」再比数字:同一 PR 内 analyzer 版本、压缩算法与是否含 map 注释必须一致,否则体积差值不可信。

webpack-bundle-analyzer 的配置与使用方式:

// webpack.config.js — 集成 bundle analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  mode: 'production',
  plugins: [
    // 仅在 ANALYZE=true 时启动,避免常规构建打开浏览器
    process.env.ANALYZE === 'true' && new BundleAnalyzerPlugin({
      analyzerMode: 'static',          // 生成静态 HTML 报告,适合 CI
      reportFilename: 'bundle-report.html',
      openAnalyzer: false,             // CI 中不自动打开浏览器
      generateStatsFile: true,         // 同时生成 stats.json
      statsFilename: 'bundle-stats.json',
    }),
  ].filter(Boolean),
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
      },
    },
  },
};

// 运行:ANALYZE=true npm run build
// CI 中保存报告为 artifact 供 PR 审查

体积预算与口径(gzip / brotli / raw)

预算建议按「路由或入口」拆开:主文档 JS、共享 vendor、懒加载路由、Worker/WASM 单独一行;SKILL 中写清团队主看哪一列(多数团队以 gzip 或 brotli 传输体积为准,raw 用于排查未压缩膨胀)。

示例预算行:占位数值,请在 SKILL 中替换为团队真实阈值
入口 / chunk gzip 上限(示例) 备注
首屏主包(HTML 关键路径) 120 KB 含框架运行时与全局样式入口则单列或拆 vendor
共享 vendor(稳定 hash) 200 KB 与业务 chunk 分离,避免业务改字即整包失效
单一路由懒 chunk 80 KB 管理端重页可单独放宽并注明角色
与预算对比(gzip,KB)

输入与表格某行预算一致的数字后,填写实测体积可得到是否超预算的提示。

填写实测 gzip 体积(KB)后显示结果。

Treemap 怎么读(面积、重复与侧链)

Treemap 中面积大的块通常对应高成本依赖或未被 tree-shake 的入口;向下钻取到具体 npm 包路径,核对是否多版本并存、是否误 import 了 dist 全量、或 sideEffects 导致整包保留。

  • 同一库出现多个色块或路径:检查 lockfile、alias、以及是否同时打了 CJS 与 ESM 两份。
  • 业务代码薄但 vendor 厚:优先查动态 import 边界与「同步 re-export」是否把重依赖拉进父 chunk。
  • 与 source map 对照:确认是「真实业务引用」还是构建插件注入的 helper/polyfill。

要点:

矩形面积(或可视化工具中的权重)反映该模块在统计口径下的体积占比;先扫最大块再逐层展开。

CI 回归与拆包动作

门禁失败时 PR 描述应包含:基线分支同配置下的对比、超预算 chunk 名称、treemap 或报告链接、以及拟议修复(换轻量 API、懒加载、拆 preset、剔除 dev 依赖进生产图等)。

  • vendor 与 app 分离,稳定 hash 利于浏览器缓存。
  • 同步脚本阻塞渲染的路径单独标红。
  • 图片与字体走 CDN 与合适格式(AVIF、woff2)。

bundlesize 在 GitHub Actions 中配置(PR 中显示包体变化):

// package.json — bundlesize 配置
{
  "bundlesize": [
    { "path": "./dist/main.*.js",    "maxSize": "120 kB" },
    { "path": "./dist/vendors.*.js", "maxSize": "200 kB" },
    { "path": "./dist/*.chunk.js",   "maxSize": "80 kB"  }
  ]
}

// .github/workflows/bundle-check.yml
// jobs:
//   bundle-size:
//     runs-on: ubuntu-24.04
//     steps:
//       - uses: actions/checkout@v4
//         with:
//           fetch-depth: 0   # 需要 main 分支做基线对比
//       - run: npm ci && npm run build
//       - name: Check bundle size
//         run: npx bundlesize
//         env:
//           BUNDLESIZE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
//           # 自动在 PR 上留评论,标出超预算的 chunk

// 动态 import 的正确写法(路由级代码分割)
// router.js
const routes = [
  {
    path: '/dashboard',
    // 正确:每个路由独立 chunk,支持 prefetch
    component: () => import(
      /* webpackChunkName: "dashboard" */
      /* webpackPrefetch: true */
      './views/Dashboard.vue'
    ),
  },
  {
    path: '/admin',
    // 管理端独立 chunk(体积可单独放宽预算)
    component: () => import(
      /* webpackChunkName: "admin" */
      './views/Admin.vue'
    ),
  },
];

Tree shaking 验证:检查哪些代码被删除了:

# 验证 tree shaking 是否生效:检查构建产物中是否包含未使用的导出
# 方法 1:bundle-stats.json + source-map-explorer
npx source-map-explorer dist/main.*.js --json > explorer.json
# 在 JSON 中搜索你认为已被 shake 掉的函数名

# 方法 2:webpack --display-used-exports(webpack 4/5)
# webpack.config.js 加 optimization.usedExports: true
# 构建时日志会标注 [used exports: x, y]

# 方法 3:检查 package.json 中是否有 sideEffects 声明
# 没有 "sideEffects": false 时,webpack 不会 tree-shake 该包
cat node_modules/lodash-es/package.json | grep sideEffects
# 应看到: "sideEffects": false

# 常见误导致 tree-shake 失效的写法:
# ❌ import _ from 'lodash'          → 引入全量 CJS 版本
# ✓  import { debounce } from 'lodash-es'  → ESM + sideEffects:false

SKILL 片段

---
name: bundle-size-cn
description: 分析前端包体、拆包与体积预算门禁
tags: [frontend, webpack, performance, bundle]
---
# 分析工具
1. webpack-bundle-analyzer:ANALYZE=true 生成静态 HTML treemap 报告
2. source-map-explorer:从 source map 还原各模块体积,验证 tree shaking
3. bundlesize:在 package.json 配置预算,PR 自动留评论

# 预算口径
4. 主口径:gzip 或 brotli 传输体积(与 CDN 实际传输一致)
5. 分行设预算:首屏主包、vendor chunk、单路由懒 chunk 分别设阈值
6. CI 中 bundlesize 或 size-limit 门禁,失败说明增长来自新依赖还是业务代码

# Tree shaking 验证
7. 确认目标包有 "sideEffects": false 且提供 ESM 入口
8. 使用具名导入(import { debounce } from 'lodash-es')而非默认导入全量
9. optimization.usedExports: true 在构建日志标注已用与未用导出

# 代码分割
10. 路由级 dynamic import():每个路由独立 chunk,配合 webpackChunkName
11. webpackPrefetch: true 在主包加载完成后预取下一页 chunk
12. 管理端重页单独 chunk,可设更宽松的独立预算
13. vendor 与 app 分离:node_modules 单独 cacheGroup,稳定 hash 利于缓存

# CI 集成
14. PR 中附 treemap 截图或报告链接,说明增长根因
15. 基线对比:与 main 分支同配置下的产物对比(fetch-depth: 0)
16. 超出预算时给出具体改法:换轻量 API、懒加载、剔除 dev 依赖

返回技能库 更多技能入口