深入理解Webpack中sideEffects配置的优化实践
先说结论,别整那些虚的
我搞过好几个中大型项目,打包优化这块没少折腾。关于 sideEffects 配置,很多人还在懵:到底要不要写?写了有什么用?tree-shaking 真靠它吗?
我的结论是:如果你在做类库(比如组件库、工具包),必须认真写 sideEffects。如果是普通业务项目,Webpack 默认行为已经够用了,但加一下也无伤大雅,还能帮你干掉一些“假依赖”。
但我真正想比的是——几种管理 sideEffects 的方式,哪种更靠谱、少踩坑。今天就拿三个常见方案开刀:
- 直接在 package.json 里写布尔值
- 用数组列明细
- 配合 Babel 插件动态处理(比如 @babel/plugin-transform-runtime)
谁更灵活?谁更省事?
先说最简单的——直接在 package.json 里写 "sideEffects": false。这个我见得最多,尤其很多开源库都这么干。
{
"name": "my-utils",
"sideEffects": false
}
意思很明确:整个包都没有副作用,随便摇吧。听起来很爽,对吧?但问题来了:真的一点副作用都没有?
我之前接手一个工具库,作者图省事直接设了 false,结果里面有个文件是这样写的:
// polyfill.js
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^s+|s+$/g, '');
};
}
这明显有副作用啊,修改了原生对象。可因为 sideEffects 设成 false,Webpack 认为这文件根本没人引用,直接给 tree-shaking 摇没了。线上一跑,低版本浏览器直接报错。
这里注意,我踩过好几次坑:**sideEffects: false 是全局声明,一旦用了就得确保每个模块都纯**。现实中很难保证,尤其引入第三方 polyfill 或 CSS import 的时候。
精细控制才是王道
所以我现在更喜欢用数组形式列出来哪些文件有副作用。虽然多写几行,但安心。
{
"sideEffects": [
"./src/polyfills.js",
"./src/styles/global.css",
"*.scss",
"*.less"
]
}
这样 Webpack 就知道:除了这些文件,其他都可以放心摇。而且支持通配符,写起来也不算太累。
举个真实例子:我们有个后台系统拆成了多个子模块,主入口 import 了一堆组件,但其实只用了其中几个。开发同事为了方便,在 index.js 里全导出了。
// components/index.js
export { default as Button } from './Button';
export { default as Modal } from './Modal';
export { default as DatePicker } from './DatePicker';
// ...还有十几个
如果我们把 sideEffects 设成 false,又没排除 CSS 文件,那像这样的样式导入就会被误删:
// Button/index.js
import './style.css'; // 这个 import 没有变量接收,会被认为无用
export { default } from './Button';
改成数组后,加上 '*.css',问题解决。不过要注意路径匹配是相对于 package.json 的,不是相对于源码目录,这点我折腾了半天才发现。
Babel 能不能救场?别太指望
有人会说,用 @babel/plugin-transform-runtime 不就能自动处理副作用了吗?确实,它能把一些 polyfill 自动替换成无副作用的导入。
{
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}
但它和 sideEffects 配置不是一回事。Babel 解决的是代码层面的污染问题,而 sideEffects 是告诉打包工具“这个模块能不能被删除”。两者可以共存,但不能互相替代。
亲测有效的是:结合使用。例如你用 transform-runtime 去除全局 polyfill,然后在 package.json 里标注剩下的副作用文件(比如全局样式)。但如果你只靠 Babel,不写 sideEffects,那 tree-shaking 效果依然打折。
还有一个坑:transform-runtime 会导致打包体积增加 runtime 辅助函数。虽然避免了污染,但代价是多了重复代码。看场景,我一般选前者,毕竟稳定性优先。
ESM vs CJS:别被格式骗了
顺便提一嘴,很多人以为只要用了 ES Module 写法,tree-shaking 就自动生效。错!
哪怕你写的是 import { foo } from 'bar',如果 bar 的 package.json 没声明 sideEffects,Webpack 会保守处理:整个模块都保留,以防万一。
这也是为什么 Vue、Lodash-es 这些库都明确写了 sideEffects。反观 lodash 普通版,因为是 CJS 输出,根本没法 tree-shaking,哪怕你只用了一个方法也会被打包进去。
所以我的建议是:写工具库一定要输出 ESM 版本,并配上正确的 sideEffects 声明。不然用户用了也白搭。
我的选型逻辑
总结一下我现在的做法:
- 如果是类库项目,一定用数组写法,精确列出所有带副作用的文件
- 如果确认真的完全无副作用(比如 pure utility 函数集合),才用
false - 永远不要假设 Babel 或 TypeScript 能帮你搞定 tree-shaking,它们不负责模块级别的删除决策
- 搭配 rollup 或 vite 构建时也要检查构建产物是否保留了 sideEffects 声明,有时候打包后 package.json 没同步更新,坑死人
另外提醒一点:测试的时候别光看本地 dev server。一定要 build 后用 source-map-explorer 或 webpack-bundle-analyzer 看实际打包结果。我改完配置以为 OK 了,结果发现某个工具函数还是没被摇掉,查半天才发现是因为被一个 HOC 组件间接引用了。
这种问题不会报错,但会让你的包越来越大。等到了 100KB 以上再回头改,成本就高了。
改完仍有点小毛病,但能忍
说实话,没有完美的方案。我现在这套数组 + ESM + 显式声明的方式,也不是 100% 安全。
比如动态导入的模块,Webpack 分析不到引用链,也可能误删。还有某些通过字符串拼接 require 的情况,sideEffects 也救不了。
但这些问题属于极端场景,日常开发中极少遇到。相比之下,把 polyfill 和全局样式保护好更重要。
有一次我们上线前做性能审计,发现某个页面加载慢了 300ms,查到最后就是因为一个没标 sideEffects 的 CSS 文件被错误保留,导致整个组件树都没被摇掉。加了一行 './styles/theme.css',体积直接小了 40KB。
所以说,这玩意儿看着小,影响不小。
以上是我的对比总结,有不同看法欢迎评论区交流
这东西没有标准答案,但我坚持认为:**宁可多写几行配置,也不要赌打包工具的智能程度**。Webpack 再聪明,也看不懂你代码背后的意图。
sideEffects 虽然只是 package.json 里一行配置,但它决定了你的代码能不能被正确地“瘦身”。尤其是在做可复用的模块时,这是对使用者最基本的尊重。
如果你也在维护一个 npm 包,别偷懒,花十分钟把 sideEffects 搞清楚。未来某个深夜排查包体积问题的人,可能就是你自己。

暂无评论