前端包体与拆包
让 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 用于排查未压缩膨胀)。
| 入口 / chunk | gzip 上限(示例) | 备注 |
|---|---|---|
| 首屏主包(HTML 关键路径) | 120 KB | 含框架运行时与全局样式入口则单列或拆 vendor |
| 共享 vendor(稳定 hash) | 200 KB | 与业务 chunk 分离,避免业务改字即整包失效 |
| 单一路由懒 chunk | 80 KB | 管理端重页可单独放宽并注明角色 |
Treemap 怎么读(面积、重复与侧链)
Treemap 中面积大的块通常对应高成本依赖或未被 tree-shake 的入口;向下钻取到具体 npm 包路径,核对是否多版本并存、是否误 import 了 dist 全量、或 sideEffects 导致整包保留。
- 同一库出现多个色块或路径:检查 lockfile、alias、以及是否同时打了 CJS 与 ESM 两份。
- 业务代码薄但 vendor 厚:优先查动态 import 边界与「同步 re-export」是否把重依赖拉进父 chunk。
- 与 source map 对照:确认是「真实业务引用」还是构建插件注入的 helper/polyfill。
要点:
矩形面积(或可视化工具中的权重)反映该模块在统计口径下的体积占比;先扫最大块再逐层展开。
在图中搜索同名包不同路径或版本号;合并解析或锁定单一版本,必要时用 bundle 的 resolve.alias 收敛。
对照路由与权限:仅用户可达路径上的 chunk 应收窄;重模块应处于 import() 子图中而非 layout 同步依赖链。
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 依赖