1. 基本概念

Tree Shaking 是一种死代码消除(Dead Code Elimination)技术,通过静态分析消除 JavaScript 应用程序中未使用的代码,从而减小最终打包文件的体积。

这个术语来源于摇树的比喻:摇动一棵树,让枯叶(未使用的代码)掉落,只保留活的枝叶(实际使用的代码)。

2. 工作原理

基于 ES6 模块系统

  • 依赖 ES6 的 importexport 语法
  • 利用静态结构分析,在编译时确定模块依赖关系
  • 标记使用的代码,移除未使用的代码
// 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 的条件

必要条件:

  1. ES6 模块语法 - 使用 import/export
  2. 静态结构 - 导入导出在编译时确定
  3. 无副作用 - 代码执行不会产生副作用
  4. 构建工具支持 - 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 的局限性

  1. 只适用于 ES6 模块
  2. 无法处理动态导入
  3. 对类的支持有限
  4. 需要构建工具支持
  5. 副作用检测不够智能