1. 基本概念
Tree Shaking 是一种死代码消除(Dead Code Elimination)技术,通过静态分析消除 JavaScript 应用程序中未使用的代码,从而减小最终打包文件的体积。
这个术语来源于摇树的比喻:摇动一棵树,让枯叶(未使用的代码)掉落,只保留活的枝叶(实际使用的代码)。
2. 工作原理
基于 ES6 模块系统
- 依赖 ES6 的
import和export语法 - 利用静态结构分析,在编译时确定模块依赖关系
- 标记使用的代码,移除未使用的代码
// math.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 - 只使用了 add 函数
import { add } from './math.js'
console.log(add(1, 2))
// 经过 Tree Shaking 后,subtract 和 multiply 会被移除3. Tree Shaking 的条件
必要条件:
- ES6 模块语法 - 使用
import/export - 静态结构 - 导入导出在编译时确定
- 无副作用 - 代码执行不会产生副作用
- 构建工具支持 - Webpack、Rollup 等
// ✅ 支持 Tree Shaking
export const utils = {
formatDate: (date) => date.toISOString(),
formatNumber: (num) => num.toLocaleString()
}
// ❌ 不支持 Tree Shaking(CommonJS)
module.exports = {
formatDate: (date) => date.toISOString(),
formatNumber: (num) => num.toLocaleString()
}
// ❌ 动态导入(运行时确定)
const moduleName = getModuleName()
import(moduleName).then(module => {
// ...
})4. Webpack 中的 Tree Shaking
配置示例:
// webpack.config.js
module.exports = {
mode: 'production', // 生产模式自动开启
optimization: {
usedExports: true, // 标记使用的导出
sideEffects: false, // 声明无副作用
minimize: true // 压缩代码
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
modules: false // 保留 ES6 模块语法
}]
]
}
}
}
]
}
}package.json 配置:
{
"name": "my-package",
"sideEffects": false,
// 或者指定有副作用的文件
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfill.js"
]
}5. 副作用(Side Effects)
什么是副作用:
- 修改全局变量
- 修改函数参数
- 发起网络请求
- 操作 DOM
- 执行 console.log 等
// 有副作用的代码
import './polyfill.js' // 修改全局对象
import './styles.css' // 影响页面样式
// 函数有副作用
let globalCounter = 0
export function increment() {
globalCounter++ // 修改全局变量
return globalCounter
}
// 无副作用的纯函数
export function add(a, b) {
return a + b // 不修改外部状态
}6. 常见问题和解决方案
问题1:Babel 转换破坏了 Tree Shaking
// 错误配置
{
"presets": [
["@babel/preset-env", {
"modules": "commonjs" // ❌ 转换为 CommonJS
}]
]
}
// 正确配置
{
"presets": [
["@babel/preset-env", {
"modules": false // ✅ 保留 ES6 模块
}]
]
}问题2:导入整个库
// ❌ 导入整个 lodash
import _ from 'lodash'
_.debounce(fn, 300)
// ✅ 按需导入
import debounce from 'lodash/debounce'
debounce(fn, 300)
// ✅ 使用支持 Tree Shaking 的版本
import { debounce } from 'lodash-es'问题3:类的方法无法被 Tree Shaking
// ❌ 类的未使用方法不会被移除
export class Utils {
static add(a, b) { return a + b }
static subtract(a, b) { return a - b } // 即使未使用也会被打包
}
// ✅ 使用独立的函数
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b } // 未使用会被移除7. 实际应用示例
创建可 Tree Shaking 的库:
// utils/index.js
export { default as debounce } from './debounce'
export { default as throttle } from './throttle'
export { default as deepClone } from './deepClone'
export { default as formatDate } from './formatDate'
// utils/debounce.js
export default function debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}使用时:
// 只会打包 debounce 相关代码
import { debounce } from './utils'
const debouncedHandler = debounce(() => {
console.log('Handler called')
}, 300)8. 构建工具对比
| 工具 | Tree Shaking 支持 | 特点 |
|---|---|---|
| Webpack | ✅ 支持 | 需要配置,依赖 UglifyJS/Terser |
| Rollup | ✅ 原生支持 | 专为 ES6 模块设计,效果最好 |
| Parcel | ✅ 支持 | 零配置,自动检测 |
| Vite | ✅ 支持 | 基于 Rollup,开发快速 |
9. 验证 Tree Shaking 效果
使用 webpack-bundle-analyzer:
npm install --save-dev webpack-bundle-analyzer// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}查看打包结果:
# 构建并分析
npm run build
# 会自动打开浏览器显示打包分析结果10. 最佳实践
1. 库开发者:
// 提供 ES6 模块版本
{
"main": "dist/index.cjs.js", // CommonJS 版本
"module": "dist/index.esm.js", // ES6 模块版本
"sideEffects": false
}2. 应用开发者:
// 使用按需导入
import { Button } from 'antd' // ❌
import Button from 'antd/lib/button' // ✅
// 配置 babel-plugin-import
{
"plugins": [
["import", {
"libraryName": "antd",
"libraryDirectory": "lib",
"style": true
}]
]
}3. 避免副作用:
// ❌ 有副作用
import './global-styles.css'
console.log('Module loaded')
// ✅ 明确标记
// package.json
{
"sideEffects": ["*.css", "*.scss"]
}11. Tree Shaking 的局限性
- 只适用于 ES6 模块
- 无法处理动态导入
- 对类的支持有限
- 需要构建工具支持
- 副作用检测不够智能