稀土掘金技术社区

理解 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 等)

工作原理简述:

  1. 构建工具解析模块依赖关系;
  2. 标记哪些导出被实际引用;
  3. 移除未被引用的导出代码(Dead Code Elimination);
  4. 压缩器(如 Terser)进一步移除无效语句。

二、一个简单示例

// math.js
exportfunctionadd(a, b) {
return a + b;
}

exportfunctionsub(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
exportconst utils = {
a: () =>console.log('A'),
b: () =>console.log('B'),
};
// main.js
import { utils } from'./utils.js';
utils.a();

✅ 「问题」:尽管只用了 a(),但整个 utils 对象都会被打包进去。

✅ 「建议」:尽可能按功能粒度导出:

exportconst a = () =>console.log('A');
exportconst b = () =>console.log('B');

3. 间接引用导致 Tree Shaking 失效(以及如何正确写)

Tree Shaking 对代码的分析是基于「静态导入路径」的。当你通过中间模块“转发”某个模块的内容时,特别是使用聚合对象导出时,可能会导致 Tree Shaking 无法判断你真正使用了什么。

// math.js
exportconst add = (a, b) => a + b;
exportconst sub = (a, b) => a - b;
// index.js
import * as math from'./math.js';
exportdefault 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
exportconst add = (a, b) => a + b;
exportconst 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. 动态语法阻止静态分析

exportconst 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 并不是魔法,而是一种依赖静态语法分析的工具优化手段。理解它的工作机制并遵循相关规则,才能让你的项目瘦身成功,跑得更快!

如果你还在为构建体积太大而苦恼,或许是时候检查一下你的代码结构,是不是也存在一些“甩不掉”的“树枝”呢?

AI编程资讯AI Coding专区指南:

https://aicoding.juejin.cn/aicoding

图片
点击"阅读原文"了解详情~