打包体积分析与优化
体积优化的前提是知道体积花在哪。先分析再动手,不然多半优化了个寂寞——把注意力放在拆分包上,结果真正的体积大头是一个被整包引入的图表库。
一、先看清体积花在哪
顺序很重要,跳步会导致优化错地方。先看产物可视化(谁最大),再看压缩后体积(真实传输多大,未压缩体积会误导,因为线上走 gzip/brotli),再按路由拆分看首屏到底加载了什么,最后查重复依赖(同一个包被装了多个版本)。很多人一上来就做代码分割,结果真正的体积大头是一个被整包引入的图表库——可视化工具一眼就能看到它占了一整块。可视化工具通常还能按「压缩后体积」重新排序,这一步能纠正「未压缩看着不大」的错觉:有些库未压缩体量大但压缩率极高,真实传输未必靠前;反之有些小库压缩后占比意外高。按真实传输量排,才不会误伤或漏判。
// 产物可视化:以 rollup 生态为例,生成可交互的体积图谱
import { visualizer } from 'rollup-plugin-visualizer'
export default {
plugins: [visualizer({ filename: 'stats.html', gzipSize: true })],
}
大头常常是一个整包引入的库,先看见它再谈怎么减
二、常见的几个体积大户
体积去哪了,常见几类:大依赖(日期处理、图表、富文本、地图这类功能密集的库),重复依赖(同一个包被装了多个大版本,两份都在 bundle 里),未按需引入(整包 re-export 导致 Tree Shaking 失效),polyfill 与降级产物(为了兼容老环境打进来的垫片),以及语言包全量(locale 全部引入)。体积分析的产出应当是一张「谁占了多少」的清单,而不是凭感觉。重复依赖最隐蔽:它常藏在传递依赖里——你只引了 A,A 却拉了一个旧版 B,和你直接引的新版 B 同时存在,两份都进包。光看源码看不到,得查 lockfile 或依赖分析命令才能揪出来。
// 依赖重复检测:找出同一包的两个版本,避免两份都进 bundle
// 以 npm 生态为例
// npx npm ls <包名> # 看同一包是否装了多个版本
// npx duplicate-package-checker-webpack-plugin # 或直接用这个 webpack 插件扫重复依赖
// 或在 package.json 锁定版本,避免传递依赖拉出第二个大版本
先看是谁占了多少,再谈怎么减,别凭感觉砍
三、按成因逐项拆
按收益排序而不是按难度:删依赖 > 换轻量实现 > 按需引入 > 分包与懒加载 > 压缩与 CDN。
- 删依赖:能自己写三十行解决的就不要引一个包。
- 换轻量实现:日期、图表、富文本这类差距最大,比如用更轻的库替代体积大的老牌库。
- 按需引入:解决整包 re-export,确认库的
sideEffects标注正确,让 Tree Shaking 真正生效。 - 分包与懒加载:路由级分割优先,改善首屏但不减小总量。
- 压缩与 CDN:收益有限但是兜底,线上走 brotli,图片走 CDN 与合适格式。
// 按需引入:只拿用到的函数,而不是 import 整个库
// ❌ 整包引入,Tree Shaking 难生效
import _ from 'lodash'
// ✅ 只引入用到的函数
import debounce from 'lodash/debounce'
这个顺序背后的逻辑是:减少总量永远优于重新分配总量——分包只是把大文件切成小块,总量没变小,首屏还是得下。
四、分包过度的问题
过多小 chunk 会增加请求数,在弱网下反而更慢——每个请求都有建连与调度开销。分包要按路由与更新频率聚合,而不是按组件一刀切。更新频率相近的放一起,这样用户二次访问时能更好地命中缓存;频繁变动的业务代码和稳定的第三方库分开,避免每次发布都把整个 vendor 打挂缓存。
按路由与更新频率聚合,按组件切是常见误区
路由级分割很常见,但要清楚它解决的是首屏而非总量:
// 把非首屏页面拆出去:首屏 JS 立刻变小
const Report = lazy(() => import('./pages/report'))
// 但总量没变——分包只是重新分配,减少总量还得靠删依赖或换更轻的实现
// 判断标准:首屏 chunk 有没有变小;如果没变,说明拆错了位置
五、把体积守住
优化完最怕的事是:几周后新需求进来,又引了一个大依赖,收益被吃回去。防回退靠两件事:体积预算门禁(超过增量直接拦住 CI)+ PR 里自动展示体积变化(每次提交附上「本次 +12KB / -3KB」的评论)。把体积当成和单测一样的一等公民,团队才会持续关注它。
// 体积门禁示意:用 size-limit 在 CI 里卡死单文件上限
// .size-limit.json
[
{ "path": "dist/assets/*.js", "limit": "150 KB" }
]
// 超限则 CI 失败,阻止合入
没有门禁,优化收益几周就被新依赖吃回去
六、把这几块拆开看
chunk 切得越细不一定越好:请求数增多会拖慢弱网加载,分包要按路由与更新频率,而不是按组件切。
gzip 之后也不该不管体积——解析与执行的成本不会随压缩消失,体积仍然影响运行性能。
优化一次更不算结束,没有门禁的话,新增依赖会在几周内把收益吃回去。
先看清体积花在哪,再看是谁占了多少,最后才谈怎么减——别凭感觉砍。
看产物构成用 webpack-bundle-analyzer 最快,一眼能定位到是哪个依赖把体积撑起来的。