一、为什么要关心 Tree-Shaking?
想象一下你在写一个电商项目,随手引入了一个工具库:
js
import { add } from "math-toolkit";
console.log(add(1, 2));
这个库很大,里面还有 multiply
、divide
、sqrt
等几十个函数。你只用到了一个 add
,但传统打包工具还是会把整包代码打进 bundle。结果呢?
👉 你的用户要白白下载几百 KB 的没用代码。
这就好比你去超市买了一瓶水,收银员却让你把整车货推回家。
Tree-Shaking 就是来解决这个问题的:只"摇"下你真正用到的代码,把没用的部分抖掉。
二、Tree-Shaking 的基本原理
Tree-Shaking 基于 ES Module 的静态分析特性 。
也就是说,打包工具在构建阶段就能看出来哪些导出函数/变量被使用了,哪些完全没用。
比如:
js
// utils.js
export function add(a, b) {
return a + b;
}
export function minus(a, b) {
return a - b;
}
// index.js
import { add } from './utils.js';
console.log(add(1, 2));
构建结果只会保留 add
,而 minus
会被摇掉。
三、副作用与 sideEffects
事情没这么简单。Tree-Shaking 并不是只看"有没有用到",还得考虑 副作用(side effects) 。
什么是副作用?就是某段代码即使你没用导出的东西,也会"顺带干点别的事"。
比如:
js
// utils.js
console.log("hello, I run anyway!");
export function add(a, b) {
return a + b;
}
哪怕你只是写了:
js
import './utils.js';
这个 console.log
依然会执行,因为文件顶层代码就是副作用。
Webpack 为了处理这种情况,引入了一个字段:
json
{
"sideEffects": false
}
-
false
:表示整个包都没有副作用,可以随便摇。 -
数组:指定哪些文件有副作用,例如:
json{ "sideEffects": ["./src/polyfill.js", "*.css"] }
这样打包器就知道:大部分文件都能安全摇掉,但 polyfill 和样式表必须保留。
四、顶层副作用的执行时机
很多人会有个疑问:
如果我
import './utils.js'
,而且在package.json
里设置了sideEffects: false
,那它的副作用什么时候执行呢?是不是执行完再被删掉?
关键点来了:
- 执行发生在浏览器运行时,而不是打包阶段。
- 删除发生在构建阶段。
换句话说,如果 Webpack 确认某个模块没有被用到,并且 sideEffects
声明它也没副作用,它在最终 bundle 里根本不会出现,自然也就不会有机会执行。
但如果 Webpack 觉得"这玩意儿可能有副作用",它会保留 import './utils.js'
对应的内容,哪怕你没用导出的函数。
所以,副作用是构建阶段就决定是否保留的,而不是运行完再删。
五、Terser 在其中扮演什么角色?
Webpack 本身只做"标记和剪枝",真正把"没用的代码删除掉"的工作,很多时候交给 Terser 这种压缩工具。
流程大概是这样的:
- Webpack 分析依赖树:知道哪些导出没被用。
- 标记未使用代码 :例如打上
/* unused harmony export ... */
。 - Terser 压缩阶段:发现这些标记的代码不会被引用,于是直接删掉。
换句话说,Tree-Shaking 是一个"协作过程":
- Webpack 做"分析和标记";
- Terser 做"实际删除"。
如果你只用 Webpack 而没开 Terser,很多"没用的函数"可能还会留在 bundle,只是"永远不会被调用"。
六、举个完整例子
假设你写了:
js
// math.js
export function add(a, b) { return a + b; }
export function minus(a, b) { return a - b; }
console.log("math module loaded");
// index.js
import { add } from './math.js';
console.log(add(1, 2));
情况 1:sideEffects: true
最终产物会保留:
add
(因为用到了)console.log("math module loaded")
(顶层副作用必须执行)minus
(没用到,会被标记并在 Terser 阶段删掉)
情况 2:sideEffects: false
Webpack 认为 math.js
没副作用,于是直接摇掉 console.log("math module loaded")
,只保留 add
。
👉 结果比情况 1 更干净。
七、常见坑点
import 'xxx'
裸引入
如果xxx
文件里有副作用(比如注册 polyfill),它一定会执行,除非你明确告诉 Webpack 它没副作用。- CommonJS 不友好
Tree-Shaking 基于 ESM 的静态分析,require()
是动态的,优化效果不如import
。 - 副作用写法要注意
比如Array.prototype.myMethod = ...
这种改全局对象的写法,Tree-Shaking 永远不会删,因为它无法保证安全。
八、最佳实践总结
- 优先使用 ESM(
import/export
),不要用 CommonJS。 - 在
package.json
里正确设置sideEffects
。 - 不要在工具函数里乱写副作用。
- 配合 Terser 等压缩器,才能看到最终瘦身效果。
九、心智模型:Tree-Shaking 本质
Tree-Shaking 不是"执行完再删",而是:
- 构建阶段:标记哪些东西没用 / 哪些有副作用必须保留。
- 压缩阶段:Terser 之类的工具把没用的代码物理删除。
- 运行阶段:浏览器执行的只有最终保留下来的代码。
所以可以把它想象成:
👉 打包工具提前帮你把代码"筛干净",让浏览器只拿到真正需要的那一部分。
十、结语
Tree-Shaking 看似只是"删除无用代码",但里面牵扯到 ESM 静态分析 、副作用标记 、压缩器配合 等多个环节。
理解了副作用执行时机、sideEffects
的作用,以及 Terser 的角色,你就能做到真正的 终极理解。
所以,下次有人问你 Tree-Shaking 是啥,你可以自信地说:
它不是"代码运行后再清理垃圾",而是"在打包阶段提前分析,把没用的东西丢掉,只把干净的代码交给浏览器"。