Webpack中sideEffects配置的实战应用与优化技巧

Good“景岩 优化 阅读 2,217
赞 27 收藏
二维码
手机扫码查看
反馈

又踩了个坑:Tree Shaking 不生效,原来是 sideEffects 没配对

前几天打包项目,发现一个奇怪的问题:明明只用了某个工具库里的一个函数,结果整个库都被打包进去了。我用的是 Webpack 5 + Babel,按理说 Tree Shaking 应该能干掉没用的代码才对。折腾了半天,最后发现是 sideEffects 配置没搞对。

Webpack中sideEffects配置的实战应用与优化技巧

一开始我还以为是自己写法有问题,比如是不是用了 import * as utils from 'my-utils' 这种全量导入?但检查后发现不是,我是这么写的:

import { debounce } from 'my-utils';

看起来完全没问题啊。Webpack 的文档里也说,只要满足 ES Module + production mode + 无副作用,就能自动 Tree Shaking。那问题出在哪?

排查过程:从怀疑人生到翻源码

我先试着在 webpack.config.js 里加了 usedExports: truesideEffects: true,但没用。然后又去查那个工具库的 package.json,发现它压根没写 sideEffects 字段。这时候我有点懵:难道默认是 true?那不就等于告诉 Webpack “这个包有副作用,别乱删”?

翻了下 Webpack 官方文档,果然:如果 package.json 里没有 sideEffects 字段,默认值是 true。也就是说,Webpack 会保守地认为所有模块都有副作用,不敢删任何代码。这下明白了——不是我的配置问题,是第三方库没声明“我无副作用”。

但问题是,这个工具库是我自己维护的,所以我可以直接改它的 package.json。不过改之前,我得先确认它到底有没有副作用。

啥叫“副作用”?别被术语吓到

其实“副作用”在这里特指:模块执行时会不会产生除了导出值以外的影响。比如:

  • 修改全局变量(window.xxx = ...
  • 发起网络请求(fetch(...)
  • 往 DOM 里插东西(document.body.appendChild(...)
  • 调用 console.log(虽然不影响功能,但属于“执行即有输出”)

如果一个模块只是纯函数,比如:

// utils.js
export const add = (a, b) => a + b;
export const debounce = (fn, delay) => { /* ... */ };

那它就是无副作用的,可以安全 Tree Shaking。

但如果你的模块长这样:

// bad-utils.js
console.log('utils loaded!'); // 副作用!
window.myUtils = { add };     // 副作用!
export { add };

那即使你只 import add,Webpack 也不敢删这段代码,因为删了可能影响全局状态。

我那个工具库,检查了一遍,全是纯函数,连 console.log 都没有。所以可以放心标记为无副作用。

核心配置就这一行

于是我在工具库的 package.json 里加了这么一行:

{
  "name": "my-utils",
  "sideEffects": false
}

注意:这里必须是 false,不是字符串 "false",也不是 null。Webpack 只认布尔值 false 表示“整个包无副作用”。

改完之后重新打包,体积立马小了一大截。用 Webpack Bundle Analyzer 看了下,确实只有 debounce 被保留,其他函数全被干掉了。搞定!

但等等,现实哪有这么简单

实际项目中,很多库并不是完全无副作用的。比如有些 UI 组件库,会在模块顶层注册全局样式或组件:

// components/button.js
import './button.css'; // 副作用!
registerComponent('Button', ButtonImpl);
export default ButtonImpl;

这时候你不能直接写 "sideEffects": false,否则 Webpack 会把 import './button.css' 这行删掉,样式就没了。

这种情况下,就得用数组形式,明确告诉 Webpack 哪些文件有副作用:

{
  "sideEffects": [
    "./src/components/**/*.css",
    "./src/polyfills.js"
  ]
}

这样,Webpack 在做 Tree Shaking 时,会跳过这些文件,其他 JS 模块照常优化。

我后来在另一个项目里就遇到这种情况。一个内部组件库,既有纯逻辑函数,又有带 CSS 导入的组件。一开始我偷懒写了 false,结果上线后按钮样式全没了,差点背锅。后来改成数组配置,才稳住。

踩坑提醒:这三点一定注意

1. 不要盲目设为 false。先确认你的模块是否真的无副作用。不确定的话,可以用 Webpack 的 --display-used-exports(旧版)或者分析打包产物,看有没有意外被删的代码。

2. 数组路径是相对于 package.json 的。比如你的源码在 src/ 目录下,那就写 "./src/**/*.css",别写成 "**/*.css",否则可能匹配不到。

3. 开发环境和生产环境行为不同。Tree Shaking 只在 production 模式生效(因为 development 模式默认不压缩、不优化)。所以测试时一定要用 npm run build 而不是 npm start

附:完整配置参考

假设你有一个工具库,结构如下:

my-utils/
├── package.json
├── src/
│   ├── index.js
│   ├── math.js
│   └── dom.js
└── dist/

其中 dom.js 会操作 DOM,有副作用;其他都是纯函数。那 package.json 应该这么写:

{
  "name": "my-utils",
  "main": "dist/index.js",
  "module": "src/index.js",
  "sideEffects": [
    "./src/dom.js"
  ]
}

注意:这里 module 字段指向源码,是为了让支持 ES Module 的打包工具(如 Webpack)能直接处理原始代码,从而启用 Tree Shaking。如果只提供编译后的 CommonJS 版本,Tree Shaking 基本就废了。

另外,如果你的库是纯 ESM 的,也可以考虑用 exports 字段配合条件导出,但那是另一个话题了,今天先不展开。

结语

以上是我踩坑后的总结。其实 sideEffects 配置本身不难,难点在于理解“副作用”的真实含义,以及在复杂项目中准确识别哪些文件有副作用。有时候为了省事,很多人直接设为 false,结果埋了雷。建议大家在配置前,先花十分钟检查下代码。

这个配置虽然小,但对打包体积影响很大,尤其在用 Lodash、Moment.js 这类大库时。现在 Lodash 已经官方支持 per-method 引入,但如果你在维护自己的工具库,别忘了加上这一行。

以上是我个人对 sideEffects 配置的完整踩坑记录,如果你有更好的方案或者遇到类似问题,欢迎评论区交流!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论