Tree Shaking 冷知识总结
Tree Shaking 核心原理
ESM 静态分析的基础
javascript
// ✅ 可以被静态分析
import { add, multiply } from './math.js'; // 只导入需要的函数
import utils from './utils.js'; // 默认导入
// ❌ 无法被静态分析
const mathModule = require('./math.js'); // CommonJS 动态特性
const { add } = mathModule; // 运行时才知道用了什么
动态导入破坏 Tree Shaking
javascript
// ❌ 动态导入让打包器无法预知
async function loadMath() {
const { add } = await import('./math.js'); // 运行时决定
return add;
}
// ❌ 条件导入也会破坏
if (process.env.NODE_ENV === 'development') {
const { debugUtils } = await import('./debug.js');
}
// ✅ 静态导入可以被分析
import { add } from './math.js';
破坏 Tree Shaking 的常见陷阱
1. console.log 的副作用
javascript
// math.js
export function add(a, b) {
console.log('add function called'); // 副作用!
return a + b;
}
export function multiply(a, b) {
console.log('multiply function called'); // 副作用!
return a * b;
}
// main.js
import { add } from './math.js';
console.log(add(1, 2));
// 结果:multiply 函数不会被移除!
// 因为它包含 console.log 副作用
解决方案:
javascript
// 使用 /*#__PURE__*/ 注释
export function multiply(a, b) {
/*#__PURE__*/ console.log('multiply function called');
return a * b;
}
// 或者在 package.json 中标记
{
"sideEffects": false // 告诉打包器这个包没有副作用
}
2. * 导入的问题
javascript
// ❌ 导入整个模块
import * as utils from './utils.js';
utils.add(1, 2); // 即使只用了 add,整个 utils 都会被打包
// ✅ 按需导入
import { add } from './utils.js';
add(1, 2); // 只打包 add 函数
3. 类和原型链的陷阱
javascript
// utils.js
export class Calculator {
add(a, b) { return a + b; }
multiply(a, b) { return a * b; }
divide(a, b) { return a / b; }
}
// main.js
import { Calculator } from './utils.js';
const calc = new Calculator();
calc.add(1, 2); // 整个 Calculator 类都会被打包,包括未使用的方法
高级 Tree Shaking 技巧
1. 使用 sideEffects 配置
json
// package.json
{
"sideEffects": [
"*.css", // CSS 文件有副作用
"./src/polyfills.js", // polyfill 有副作用
"./src/global-setup.js"
]
}
2. 函数式编程友好
javascript
// ✅ Tree Shaking 友好
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
// ❌ 不够友好
export default {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
3. 条件导出的处理
javascript
// ❌ 运行时条件
export const utils = process.env.NODE_ENV === 'development'
? { debug: true, log: console.log }
: { debug: false, log: () => {} };
// ✅ 构建时条件(通过打包器处理)
export const utils = __DEV__
? { debug: true, log: console.log }
: { debug: false, log: () => {} };
实际案例分析
1. Lodash 的 Tree Shaking
javascript
// ❌ 导入整个 lodash(~70KB)
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ 按需导入(~2KB)
import debounce from 'lodash/debounce';
debounce(fn, 300);
// ✅ 使用 lodash-es(ESM 版本)
import { debounce } from 'lodash-es';
debounce(fn, 300);
2. 第三方库的陷阱
javascript
// 某些库的内部实现可能破坏 Tree Shaking
// moment.js 就是典型例子
import moment from 'moment'; // 整个库都会被打包
// 更好的选择
import { format } from 'date-fns'; // 只打包需要的函数
调试 Tree Shaking
1. Webpack Bundle Analyzer
bash
npm install --save-dev webpack-bundle-analyzer
javascript
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
2. 查看打包结果
bash
# Webpack
npx webpack --mode=production --optimization-used-exports
# Rollup
npx rollup -c --silent
# Vite
npx vite build --mode=production
最佳实践总结
- 优先使用 ESM:
import/export
而不是require
- 按需导入:避免
import *
和默认导出对象 - 标记副作用:正确配置
sideEffects
- 避免运行时逻辑:在导入语句中避免条件判断
- 使用 Pure 注释:对有副作用但安全的代码使用
/*#__PURE__*/
- 选择合适的库:优先选择支持 Tree Shaking 的库版本
常见误区
误区1:以为所有 ESM 都支持 Tree Shaking
javascript
// 即使是 ESM,如果有副作用也不会被 shake 掉
export const config = {
apiUrl: process.env.API_URL || 'default' // 运行时逻辑
};
误区2:认为 default export 不支持 Tree Shaking
javascript
// ✅ default export 也可以被 shake
export default function add(a, b) {
return a + b;
}
// 问题在于导出对象
export default {
add: (a, b) => a + b,
multiply: (a, b) => a * b // 即使不用也会被打包
};
误区3:忽略第三方库的 Tree Shaking 支持
javascript
// 检查库是否支持 Tree Shaking
// 1. 查看 package.json 的 sideEffects 字段
// 2. 查看是否提供 ESM 版本
// 3. 使用 bundle analyzer 验证效果
总结
Tree Shaking 是现代前端优化的重要技术,理解这些细节能帮你写出更高效的代码。关键是要理解静态分析的限制,避免副作用,选择合适的导入导出方式。