打开Chrome DevTools,看到一个2MB的压缩包,你可能觉得"还行"。但在一部中端安卓机上,V8引擎要花12秒解析编译——用户盯着白屏,连按钮都点不了。
这不是边缘场景。这是全球60%用户的日常。
打包器的默认陷阱
运行npm run build时,Rollup、Webpack或esbuild会做一件事:把所有东西塞进一个文件。你的组件、工具函数、整个lodash库、一千个图标——全部打包。
为什么默认这么做?因为HTTP请求有开销。一个文件意味着一次连接、一份缓存、最少的往返。对小型应用,这是对的优化。
但当你的vendor代码超过应用代码,当某个日期库因为一次格式化调用拖进300KB,当用户只需要首页却下载了整个单页应用——"一个大文件"就从资产变成负债。
先看见,才能优化
别猜。用工具看。
rollup-plugin-visualizer给Vite/Rollup项目生成交互式树图。配置里加几行,build完打开HTML,矩形面积就是模块体积。一眼扫过去,vendor块有没有意外之客,清清楚楚。
Webpack用户用webpack-bundle-analyzer。装完跑两条命令,JSON转可视化,同样的树图逻辑。
如果连插件都懒得配,source-map-explorer直接分析产物文件。有source map就能跑。
看图时盯三个信号:单个文件超过100KB的vendor依赖、重复出现的同类库(比如两个日期处理库)、只在特定路由用到的代码却躺在主包里。
摇树:为什么有时候摇不掉
Tree shaking(摇树优化)靠ES模块的静态结构。导入导出在编译时确定,没用到的代码理论上该被剔除。
但"理论上"经常翻车。常见死因:CommonJS混用、动态属性访问、副作用未声明。
package.json里的sideEffects字段是控制阀。设为false,打包器敢大胆删;设为数组,列出的文件保留,其余优化。很多库没配这个,导致整包保留——哪怕你只导入了一个函数。
代码分割:按需的哲学
动态import()是分割点。写法像普通导入,但返回Promise,打包器自动拆成独立chunk。
用户点到"设置"页面才去下载设置页的代码。首屏包瘦身,交互时间骤降。这不是懒加载的装饰,是性能预算的重新分配。
路由级分割、组件级分割、甚至函数级分割——粒度越细,初始下载越小。代价是运行时管理更多请求,需要权衡。
一个检查清单
下次发版前,跑一遍:用可视化工具扫描包体积、检查sideEffects配置、把路由改成动态导入、验证3G下的首屏时间。
2MB的gzip包在开发者电脑上秒开,在用户手里是12秒的沉默。这个差距,就是优化空间。
热门跟贴