理解 Tree Shaking:原理与那些“甩不掉”的代码陷阱
在现代前端构建中,“Tree Shaking”是一项至关重要的优化技术。它的目标是在打包过程中移除那些未被使用的“死代码”,从而减小最终 bundle 的体积,提升加载性能。
本文将深入剖析 Tree Shaking 的原理,并结合实际代码示例讲解一些常见的“甩不掉”的坑。
一、什么是 Tree Shaking?
Tree Shaking 是一种 静态分析(Static Analysis) 技术,用于在构建过程中移除未被引用的 ES 模块导出内容。
前提条件:
- 必须使用 ES Module(即 ES6 的
import
/export
语法) - 必须启用 生产模式构建(如 webpack 的
mode: 'production'
) - 构建工具支持 Tree Shaking(如 Webpack、Rollup、esbuild、Vite 等)
工作原理简述:
- 构建工具解析模块依赖关系;
- 标记哪些导出被实际引用;
- 移除未被引用的导出代码(Dead Code Elimination);
- 压缩器(如 Terser)进一步移除无效语句。
二、一个简单示例
// math.js
export function add(a, b) {
return a + b;
}
export function sub(a, b) {
return a - b;
}
// main.js
import { add } from './math.js';
console.log(add(2, 3));
构建后,如果使用了 Tree Shaking,sub()
方法将不会被打包进最终 bundle。
三、Tree Shaking 常见失效案例
1. 使用 CommonJS(require
)
// bad-math.js (CommonJS)
exports.add = (a, b) => a + b;
exports.sub = (a, b) => a - b;
// main.js
const { add } = require('./bad-math');
console.log(add(1, 2));
✅ 问题:Tree Shaking 无法分析 CommonJS 模块导出内容(因其导出是动态对象,静态分析无法进行)。
✅ 建议:统一使用 import/export
的 ES Module 语法。
2. 导出对象或数组,统一挂载多个功能
// utils.js
export const utils = {
a: () => console.log('A'),
b: () => console.log('B'),
};
// main.js
import { utils } from './utils.js';
utils.a();
✅ 问题:尽管只用了 a()
,但整个 utils
对象都会被打包进去。
✅ 建议:尽可能按功能粒度导出:
export const a = () => console.log('A');
export const b = () => console.log('B');
3. 间接引用导致 Tree Shaking 失效(以及如何正确写)
Tree Shaking 对代码的分析是基于静态导入路径的。当你通过中间模块“转发”某个模块的内容时,特别是使用聚合对象导出时,可能会导致 Tree Shaking 无法判断你真正使用了什么。
// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
// index.js
import * as math from './math.js';
export default math;
// main.js
import math from './index.js';
math.add(1, 2);
✅ 问题:
你可能只用了 add
,但由于 math
是整个对象,打包工具无法静态分析 math.sub
是否会在运行时被用到。因此它会保留整个模块的内容(包括 sub
)。
✅ 建议:在写模块聚合的时候
- 避免 default export 聚合对象
- 使用
export { xxx } from
的形式将原始模块静态 re-export
// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
// index.js
export { add, sub } from './math.js'; // 直接 re-export 静态绑定
// main.js
import { add } from './index.js';
add(1, 2);
这样,构建工具可以静态分析 add
的引用。sub
没被引用,会被安全地移除。
4. 使用副作用模块(Side Effects)
js
// side.js
console.log('I will always run');
export const x = 1;
✅ 问题:即使你不使用 x
,这个模块仍然会被打包进来,因为它有副作用(console.log()
)。
✅ 解决方案:
- 在
package.json
中配置"sideEffects": false
,告诉构建工具默认模块无副作用; - 对于有副作用的模块明确标注
"sideEffects": ["./side.js"]
5. 动态语法阻止静态分析
export const tools = {};
tools['run'] = () => console.log('run');
tools['build'] = () => console.log('build');
✅ 问题:动态属性访问使构建工具无法判断哪个属性被使用,只能保留整个对象。
✅ 建议:使用静态结构进行导出。
四、如何判断 Tree Shaking 是否生效?
方法一:查看构建产物体积
- 使用
webpack-bundle-analyzer
、source-map-explorer
等工具分析 bundle; - 比较仅引入部分函数和引入整包的体积差异。
方法二:查看产物代码
- 打开打包后文件;
- 搜索你不希望打包进去的函数名,看是否仍然存在。
五、总结
错误用法 | 原因 | 建议修复方式 |
---|---|---|
使用 require()
|
动态导入,静态分析失败 | 改为 ES Module 的 import
|
聚合导出为对象 | 无法移除未用属性 | 单独导出每个函数/变量 |
使用 export * + 再导出 |
无法精准分析具体依赖 | 直接从源文件 import 所需内容 |
动态属性赋值 | 静态分析工具无法识别 | 改为静态结构导出 |
模块含副作用 | 构建工具不敢随意删除 | 使用 sideEffects 明确标注 |
六、结语
Tree Shaking 并不是魔法,而是一种依赖静态语法分析的工具优化手段。理解它的工作机制并遵循相关规则,才能让你的项目瘦身成功,跑得更快!
如果你还在为构建体积太大而苦恼,或许是时候检查一下你的代码结构,是不是也存在一些“甩不掉”的“树枝”呢?