2.3 框架要做到良好的 Tree-Shaking
Tree-Shaking 能剔除永不被执行的代码,但前提是模块必须是 ESM,且调用不能有副作用——这两点直接决定了框架源码该怎么写。
一、什么是 Tree-Shaking
- Tree-Shaking 指的是消除那些永远不会被执行的代码,也就是排除 dead code。
- 必须满足的条件:模块必须是 ESM(ES Module),因为 Tree-Shaking 依赖 ESM 的静态结构(
import/export在编译期就能确定依赖关系,而 CJS 的require是运行时行为,无法静态分析)。
二、第二个关键点:副作用
- 如果一个函数调用会产生副作用,那么就不能将其移除。
- 副作用就是调用函数时对外部产生影响,例如修改了全局变量。
- 典型场景:用
Proxy创建的代理对象,读取对象属性时会触发代理对象的 get 夹子(trap),而 get 夹子中是可能产生副作用的。 - 到底会不会产生副作用,只有代码真正运行的时候才能知道——JavaScript 本身是动态语言,所以打包工具只能保守处理。
三、Tree-Shaking 如何工作
// input.js
import { foo } from './utils.js'
foo()
// utils.js
export function foo(obj) {
obj && obj.foo
}
export function bar(obj) {
obj && obj.bar
}
执行构建:
npx rollup input.js -f esm -o bundle.js
产物 bundle.js 中没有使用 bar 函数,因此它作为 dead code 被删除:
function foo(obj) {
obj && obj.foo
}
foo()
四、/*#__PURE__*/ 注释
当打包工具无法确定某个调用是否产生副作用时,用注释显式声明:
/*#__PURE__*/ foo()
- 作用是告诉 rollup.js:对
foo函数的调用不会产生副作用,可以放心地对其进行 Tree-Shaking。 - 通常产生副作用的代码都是模块内函数的顶级调用,所以该注释基本都加在顶级调用的函数上。
- 该注释不仅仅作用于函数,它可以应用于任何语句。
- 也不是只有 rollup.js 能识别,webpack 以及压缩工具(如 terser)都能识别它。
foo() // 顶级调用:模块加载时就执行,可能产生副作用
function bar() {
foo() // 函数内调用:bar 没被使用时,整块一起被摇掉
}
五、易错点
源码里「看起来没用到」的代码不一定能摇掉:只要打包工具判断该调用可能产生副作用,就会整个保留。 反过来,
/*#__PURE__*/是承诺——如果被标记的调用真的有副作用,就会被错误删除,必须确认后再加。
速查
# 用 rollup 构建,观察 Tree-Shaking 结果(ESM 格式输出到 bundle.js)
npx rollup input.js -f esm -o bundle.js
/*#__PURE__*/ foo() // 声明该调用无副作用,允许打包工具摇掉
| 场景 | 关键点 |
|---|---|
| 代码摇不掉 | 确认模块是 ESM;确认调用是否被判为有副作用 |
| 顶级调用被保留 | 在调用前加 /*#__PURE__*/ 声明无副作用 |
相关
- 2.2 控制框架代码的体积:
__DEV__分支最终靠 Tree-Shaking 与压缩彻底消失。 - 2.4 框架应该输出怎样的构建产物:为什么必须专门提供 ESM 产物。