前端构建产物分析:如何分析打包体积并定位冗余依赖
结论:分析打包体积只需四步——先看 gzip/Brotli 压缩后的传输体积,用可视化工具把体积定位到具体模块,用依赖树命令揪出重复安装的同名包,最后按「按需引入 / 换库 / 代码分割 / 动态加载」修掉,并在 CI 里用 size-limit 之类工具卡住体积回归。只看 min 体积、不看压缩后体积,是最常见的误判来源。
分析打包体积,第一步该看哪个数字?
结论:判断一个包值不值得留,看 gzip 后体积,不看源码行数也不看 min 体积。
浏览器实际下载的是压缩后产物,所以 gzip(或 Brotli)体积才等于用户的流量成本。经验值是 Brotli 比 gzip 再小 15%~20%,只要服务端和 CDN 支持,优先开 Brotli。同一份产物,min 体积 290 KB 的库 gzip 后可能只有 70 KB,只看 min 数字会做出错误决策。
具体做法:构建完先扫一眼产物目录,dist/assets/*.js 里超过 200 KB(gzip 前)的 chunk 都值得点开看看。Vite 用户直接跑 npx vite-bundle-visualizer,Next.js 用户装 @next/bundle-analyzer 后执行 ANALYZE=true next build。
打包体积怎么可视化?四个命令直接抄
结论:不同构建器对应不同分析插件,命令都是现成的,不需要改业务代码。
- Webpack:
npm i -D webpack-bundle-analyzer,然后webpack --profile --json > stats.json再npx webpack-bundle-analyzer stats.json,会起一个本地 8888 端口的 treemap 页面,方块面积就是模块体积,点进去能看到是哪一层依赖把它带进来的。 - Vite / Rollup:
rollup-plugin-visualizer加进vite.config.ts,配置visualizer({ gzipSize: true, brotliSize: true, template: 'treemap' }),构建后自动生成stats.html。 - 已有 sourcemap 的产物:
npx source-map-explorer 'dist/**/*.js',不用改构建配置,直接反查每个文件里各模块的真实占比,特别适合排查「不知道体积从哪来」的存量项目。 - esbuild:
esbuild src/index.ts --bundle --minify --metafile=meta.json --analyze,--analyze直接输出占用最大的模块列表。
怎么定位冗余依赖和重复打包?
结论:冗余依赖分两类——同名包被装了多个版本,以及同一个库被整包引入却只用了两三个函数。
第一类用依赖树命令查:pnpm 项目跑 pnpm why lodash,npm 项目跑 npm ls lodash,yarn 用 yarn why lodash。如果输出里出现 lodash@4.17.20 和 lodash@4.17.21 两条路径,说明两个版本都被打进了产物。Webpack 项目还可以加 duplicate-package-checker-webpack-plugin,构建时直接把重复包警告打出来。
第二类看可视化图最直接:树图里某个库占了一大块,但你在代码里只 import 了它的一两个方法,就是整包引入。典型例子是 import _ from 'lodash'(完整版 min 约 71 KB)和 import moment from 'moment'(min 约 290 KB、gzip 约 70 KB,还没算 locale)。换成 lodash-es 按需导入、或者干脆换 dayjs(min 约 6.5 KB、gzip 约 2.9 KB),体积差就是几百 KB 级别。
定位到问题后,怎么把体积降下来?
结论:按「按需引入 → 换轻量替代 → 代码分割 → 动态加载」的顺序改,性价比从高到低。
- 按需引入:组件库走 ESM 的具名导入,例如
import { Button } from 'antd'而不是import antd from 'antd';确认package.json的sideEffects字段没有误标成false之外的值,否则 tree-shaking 会失效;Babel 用户检查@babel/preset-env的modules: false,让打包器能静态分析 ESM。 - 统一重复版本:在 pnpm 里用
pnpm.overrides,npm 8+ 用overrides,yarn 用resolutions,把子依赖里的旧版本强制对齐到根版本。改完务必重新跑一遍可视化,确认重复块消失。 - 代码分割:Webpack 的
splitChunks或 Vite 的build.rollupOptions.output.manualChunks,把 React、图表库这类长期不变的依赖单独拆 chunk,利用浏览器长缓存。注意:拆包不减少总体积,只优化首屏加载,别把它当成减体积手段。 - 动态加载:路由级
import()、弹窗里的富文本编辑器、只在点击后才用到的图表库,全部改成懒加载,首屏体积能直接掉一大块。
怎么防止体积反弹?
结论:把体积门禁放进 CI,比事后排查便宜得多。
用 size-limit:npm i -D size-limit @size-limit/preset-app,在 package.json 里配 "size-limit": [{ "path": "dist/assets/*.js", "limit": "170 kB" }],CI 里跑 npx size-limit,超限直接让流水线失败。阈值建议按当前实际 gzip 体积上浮 5%~10% 来定,定得太松等于没有门禁。
另外把「禁止整包引入」写进 ESLint,用 eslint-plugin-import 的 no-restricted-imports 拦住 import _ from 'lodash' 这类写法,比靠人自觉可靠。
总结:分析打包体积的核心动作是「先看 gzip、再可视化定位、再用 why 查重复、再按四招修」,最后用 size-limit 在 CI 上锁住。工具链就那几个命令,真正花时间的是判断哪个大块头值得动——优先处理 gzip 后超过 30 KB 且首屏就会加载的模块。
原文链接:https://www.gj0.com/thread-445.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。