Tree Shaking 基础知识
什么是 Tree Shaking?
Tree Shaking (摇树优化)是一种通过静态分析消除 JavaScript 中未使用代码(Dead Code)的优化技术。
形象比喻:
想象一棵树(代码库):
🌳 整棵树 = 完整的代码库
🍂 枯叶 = 未使用的代码
🤝 摇树 = 构建工具分析
✨ 结果 = 只保留需要的代码
工作原理
1. 静态分析(编译时)
Tree Shaking 依赖于 ES Module (ESM) 的静态结构特性:
javascript
// ✅ ESM - 支持 Tree Shaking(静态导入)
import { funcA } from './utils'; // 编译时确定
// ❌ CommonJS - 不支持 Tree Shaking(动态导入)
const { funcA } = require('./utils'); // 运行时确定
关键区别:
| 特性 | ESM | CommonJS |
|---|---|---|
| 导入时机 | 编译时(静态) | 运行时(动态) |
| 结构分析 | ✅ 可分析 | ❌ 不可分析 |
| Tree Shaking | ✅ 支持 | ❌ 不支持 |
| 语法 | import/export |
require/module.exports |
2. 依赖图分析
构建工具如何工作:
javascript
// utils.js
export function add(a, b) { return a + b; } // 被使用 ✅
export function subtract(a, b) { return a - b; } // 未使用 ❌
export function multiply(a, b) { return a * b; } // 未使用 ❌
// main.js
import { add } from './utils'; // 只导入 add
console.log(add(1, 2));
分析过程:
csharp
1. 入口分析:main.js 导入了什么?
→ { add } from './utils'
2. 依赖追踪:add 函数依赖什么?
→ 无其他依赖
3. 标记清除:
✅ add → 保留(被使用)
❌ subtract → 删除(未使用)
❌ multiply → 删除(未使用)
4. 最终产物:
只包含 add 函数
3. 副作用(Side Effects)处理
什么是副作用?
副作用是指除了返回值之外,还会影响外部状态的操作:
javascript
// ❌ 有副作用的代码
import './polyfill'; // 修改全局对象
import './styles.css'; // 注入样式
console.log('Module loaded'); // 控制台输出
window.myGlobal = 123; // 修改 window
// ✅ 无副作用的代码(纯函数)
export function add(a, b) {
return a + b; // 只返回值,不影响外部
}
常见的副作用:
- 修改全局变量或对象
- 修改函数参数
- 发起网络请求
- DOM 操作
- 原型链修改
- 导入 CSS 文件
- 执行 IIFE(立即执行函数)
javascript
// 修改全局变量或对象
window.myGlobalVar = 'value';
global.config = { theme: 'dark' };
// 修改函数参数
function addProperty(obj) {
obj.newProp = 'value';
}
// 发起网络请求
fetch('/api/data');
// DOM 操作
document.body.classList.add('loaded');
// 原型链修改
Array.prototype.myMethod = function() {};
// 导入 CSS 文件
import './styles.css';
// 执行 IIFE(立即执行函数)
(function() {
console.log('初始化');
})();
为什么要标记副作用?
json
// package.json
{
"sideEffects": false // 告诉打包工具:"所有代码都无副作用"
}
示例:
javascript
// utils.js
export function add(a, b) { return a + b; }
// polyfill.js
Array.prototype.includes = function() { ... }; // 有副作用!
// main.js
import { add } from './utils'; // 使用 utils
import './polyfill'; // 不使用,但有副作用
// ❌ 如果 sideEffects: false
// → polyfill.js 被删除(认为无副作用)
// → 导致运行时错误!
// ✅ 如果 sideEffects: ["**/polyfill.js"]
// → polyfill.js 被保留(标记为有副作用)
// → 运行正常
副作用代码与sideEffects配置的关系
-
副作用代码,这是客观事实,也就是说一段代码是否是副作用代码,已经是确定的。
-
package.json sideEffects,文件级别配置,用来人为声明文件是否"可能有副作用",从而决定未引用的文件能否被整体删除。- 默认为
true,则编译器不敢假设文件无副作用,所以不会删除未引用的文件。属于保守策略。 - 如果配置为
false,则编译器会把所有文件都视为无副作用文件,只要文件没有被import,就可以被整个删除。属于激进策略 - 如果配置为文件数组,则配置的文件会认为有副作用,即使文件没被引用也会被保留和打包;其他文件认为没有副作用代码,如果文件没被引用则编译会被移除不会被打包。比如在库中,一般CSS文件和polyfill文件就要被包含在内,比如:
["*.css", "src/polyfill.js"]
- 默认为
Tree Shaking 的前提条件
✅ 必须满足的条件
- 使用 ESM 模块格式
json
// package.json
{
"type": "module",
"module": "dist/esm/index.js"
}
-
构建工具支持
- Webpack 4+
- Rollup(原生支持)
- Vite(基于 Rollup)
- esbuild
-
正确配置 sideEffects
json
{
"sideEffects": false, // 无副作用
// 或
"sideEffects": ["**/*.css"] // 只有 CSS 有副作用
}
- 避免动态导入(在需要 Tree Shaking 的地方)
javascript
// ❌ 动态导入 - 无法 Tree Shaking
const moduleName = 'utils';
import(`./${moduleName}`);
// ✅ 静态导入 - 可以 Tree Shaking
import { func } from './utils';
常见阻碍 Tree Shaking 的情况
1. 使用 CommonJS
javascript
// ❌ 不支持 Tree Shaking
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};
// ✅ 支持 Tree Shaking
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
2. 默认导出整个对象
javascript
// ❌ 难以 Tree Shaking
export default {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};
// 用户使用
import utils from './utils';
utils.add(1, 2); // 整个对象都被导入
// ✅ 易于 Tree Shaking
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// 用户使用
import { add } from './utils'; // 只导入需要的
3. 副作用未正确标记
javascript
// styles.css
.button { color: red; }
// Component.js
import './styles.css'; // 副作用:注入样式
export function Component() { ... }
// package.json
{
"sideEffects": false // ❌ 错误!CSS 会被删除
}
// ✅ 正确配置
{
"sideEffects": ["**/*.css"] // CSS 不会被删除
}
4. 类和构造函数
javascript
// ❌ 类的方法可能无法 Tree Shaking
class Utils {
add(a, b) { return a + b; }
subtract(a, b) { return a - b; }
}
// 用户使用
import { Utils } from './utils';
const u = new Utils();
u.add(1, 2); // 整个类都被导入
// ✅ 更好的方式(纯函数)
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
Tree Shaking 实战示例
示例 1:基础 Tree Shaking
库代码:
javascript
// math-lib/index.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }
用户代码:
csharp
// app.js
import { add } from 'math-lib';
console.log(add(1, 2));
打包结果:
javascript
// bundle.js(简化)
function add(a, b) { return a + b; }
console.log(add(1, 2));
// ✅ subtract、multiply、divide 被删除
示例 2:组件库 Tree Shaking
库代码:
javascript
// ui-lib/index.js
export { Button } from './Button';
export { Input } from './Input';
export { Select } from './Select';
export { Modal } from './Modal';
// ui-lib/Button.js
import './Button.css'; // 副作用
export function Button() { ... }
// package.json
{
"sideEffects": ["**/*.css"] // 保护 CSS
}
用户代码:
javascript
// app.js
import { Button } from 'ui-lib';
Button();
打包结果:
javascript
// bundle.js
import './Button.css'; // ✅ CSS 被保留(标记为副作用)
function Button() { ... }
Button();
// ✅ Input、Select、Modal 及其 CSS 都被删除
Tree Shaking 效果验证
方法 1:分析打包结果
r
# Webpack
npm run build -- --analyze
# Rollup
rollup -c --plugin visualizer
# Vite
vite build --mode production
# 查看 dist/stats.html
方法 2:手动检查
arduino
// 1. 构建生产版本
npm run build
// 2. 搜索未使用的代码
grep -r "unusedFunction" dist/
// 3. 如果找到 → Tree Shaking 失败
// 如果未找到 → Tree Shaking 成功
方法 3:对比体积
javascript
// 测试 1:引入所有
import * as All from 'my-lib';
// 打包体积:100 KB
// 测试 2:只引入一个
import { Button } from 'my-lib';
// 打包体积:20 KB
// ✅ Tree Shaking 有效:减少 80%
为库开发者的 Tree Shaking 检查清单
✅ 构建配置
- 使用 ESM 输出格式(
format: 'es') - 配置
preserveModules: true(保留模块结构) - 设置
package.json的module字段 - 设置
package.json的sideEffects字段
✅ 代码规范
- 使用
export而非export default {} - 避免在模块顶层执行副作用代码
- 纯函数优于类(函数更容易 Tree Shaking)
- 将 CSS 导入标记为副作用
✅ 文档说明
- 在 README 中说明支持 Tree Shaking
- 提供按需引入的示例
- 标注哪些文件有副作用
常见误区
❌ 误区 1:"压缩 = Tree Shaking"
javascript
// 压缩(Minify):缩短变量名、删除空格
function add(a,b){return a+b}
// Tree Shaking:删除未使用的代码
// 如果 add 没被用,整个函数被删除
// 两者不同!
❌ 误区 2:"webpack 自动 Tree Shaking"
javascript
// ❌ 不会自动 Tree Shaking(CommonJS)
const lib = require('my-lib');
// ✅ 才能 Tree Shaking(ESM)
import { func } from 'my-lib';
❌ 误区 3:"设置 sideEffects: false 就够了"
arduino
// package.json
{ "sideEffects": false }
// ❌ 但代码中有副作用
import './polyfill'; // 会被删除!
import './styles.css'; // 会被删除!
// ✅ 正确配置
{ "sideEffects": ["**/polyfill.js", "**/*.css"] }
小结
Tree Shaking 核心要点:
- 基于 ESM 的静态分析
- 编译时 确定依赖关系
- 删除 未使用的代码
- 保留 有副作用的代码
- 需要 构建工具、模块格式、配置三者配合
记忆口诀:
arduino
ESM 是前提(import/export)
静态分析为基础(编译时确定)
标记副作用要清楚(sideEffects)
保留结构才有效(preserveModules)