tree-shaking,“摇” 掉没用的代码
约 983 字大约 3 分钟
2023-06-04
什么是 Tree-Shaking?
Tree-Shaking 是一个现代 JavaScript 打包工具中广泛使用的术语,它指的是在打包过程中移除未使用的代码的技术。这个名称非常形象——就像摇动一棵树,让枯叶(无用代码)落下,只保留健康的枝叶(有用代码)。
为什么需要 Tree-Shaking?
在传统的前端开发中,我们经常引入整个库或模块,但实际上只使用了其中一小部分功能。比如:
import { filter, map, reduce } from 'lodash'
// 只使用了 map 和 filter
const result = array.map(x => x * 2).filter(x => x > 10)
上面的例子中,reduce
函数被导入但从未使用,Tree-Shaking 可以将其从最终打包文件中移除。
Tree-Shaking 的工作原理
1. 静态分析
Tree-Shaking 依赖于 ES6 模块的静态结构特性:
// ES6 模块 - 支持 Tree-Shaking
import { functionA } from './moduleA'
export { functionB } from './moduleB'
// CommonJS - 不支持 Tree-Shaking
const moduleA = require('./moduleA')
module.exports = { functionB }
ES6 模块在编译时就能确定导入导出的关系,而 CommonJS 在运行时才能确定。
2. 依赖图构建
打包工具会构建模块依赖图:
Entry → Module A → Module B → Module C
→ Module D (未使用)
3. 标记和清除
工具会标记所有从入口点可达的代码,然后清除未被标记的代码。
实际配置示例
Webpack 配置
// webpack.config.js
module.exports = {
mode: 'production', // 生产模式自动开启 Tree-Shaking
optimization: {
usedExports: true,
minimize: true,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: false }] // 重要:不转换 ES6 模块
]
}
}
}
]
}
}
Rollup 配置
// rollup.config.js
export default {
input: 'src/main.js',
output: {
file: 'dist/bundle.js',
format: 'esm' // 输出 ES 模块格式
},
plugins: [
// Rollup 默认支持 Tree-Shaking
]
}
最佳实践指南
1. 使用 ES6 模块语法
// ✅ 推荐 - 支持 Tree-Shaking
import { specificFunction } from 'library'
export function util1() {}
export function util2() {}
// ❌ 避免 - 不支持 Tree-Shaking
const _ = require('lodash')
module.exports = everything
2. 配置 Babel 正确
// .babelrc
{
"presets": [
["@babel/preset-env", {
"modules": false // 保持 ES6 模块不变
}]
]
}
3. 使用支持 Tree-Shaking 的库
选择提供 ES 模块版本的库:
{
"dependencies": {
"lodash-es": "^4.17.21", // lodash 的 ES 模块版本
"date-fns": "^2.29.3" // 天然支持 Tree-Shaking
}
}
4. 避免副作用
标记纯模块:
// package.json
{
"name": "your-package",
"sideEffects": false
}
// package.json
{
"name": "your-package",
"sideEffects": [
"*.css",
"*.scss"
]
}
常见陷阱和解决方案
1. 动态导入问题
// ❌ 无法 Tree-Shaking
const moduleName = 'utils'
import(moduleName).then((module) => {
module.dynamicFunction()
})
// ✅ 明确的静态导入
import { specificUtil } from './utils'
2. 类的方法移除
class Calculator {
add(a, b) { return a + b }
multiply(a, b) { return a * b } // 如果未使用会被移除
}
// 只使用 add 方法
const calc = new Calculator()
calc.add(1, 2)
3. 第三方库的副作用
// 某些库可能有副作用,阻止 Tree-Shaking
import 'polyfill-library' // 可能包含副作用
性能对比示例
让我们看一个实际的 Tree-Shaking 效果对比:
Tree-Shaking 前:
import _ from 'lodash'
// 只使用一个函数
const result = _.chunk([1, 2, 3, 4], 2)
// 打包大小:~70KB(整个 lodash)
Tree-Shaking 后:
import { chunk } from 'lodash-es'
const result = chunk([1, 2, 3, 4], 2)
// 打包大小:~2KB(仅 chunk 函数)
调试 Tree-Shaking
Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
总结
Tree-Shaking 是现代前端优化的关键技术,要充分发挥其作用需要:
- 使用 ES6 模块作为项目标准
- 正确配置构建工具,避免意外转换模块语法
- 选择支持 Tree-Shaking 的第三方库
- 合理标记副作用,帮助打包工具识别
- 定期分析打包结果,持续优化
通过合理运用 Tree-Shaking,可以显著减少最终打包文件的大小,提升应用加载性能,为用户提供更好的体验。