Tree Shaking 原理

理解 Tree Shaking 的静态分析和死代码消除机制

问题

Tree Shaking 是如何工作的?为什么它依赖 ES Module 的静态分析?

解答

什么是 Tree Shaking

Tree Shaking 是一种通过静态分析移除 JavaScript 中未使用代码的优化技术。名字来源于”摇树”——把树上的枯叶(无用代码)摇下来。

为什么依赖 ES Module

// ES Module - 静态结构,编译时确定依赖
import { add } from './math.js'

// CommonJS - 动态结构,运行时确定依赖
const add = require('./math.js').add

ES Module 的 import/export 是静态的:

  • 必须在模块顶层
  • 模块路径不能是变量
  • 导入导出在编译时就能确定
// ❌ 这些在 ES Module 中不允许
if (condition) {
  import { foo } from './foo.js' // 语法错误
}

import { bar } from getPath() // 语法错误

CommonJS 是动态的,无法在编译时分析:

// ✅ CommonJS 允许动态导入
if (condition) {
  const foo = require('./foo.js')
}

const bar = require(getPath())

Tree Shaking 工作流程

// 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
}

// index.js - 只使用了 add
import { add } from './math.js'
console.log(add(1, 2))

打包后,subtractmultiply 会被移除:

// 打包结果(简化)
function add(a, b) {
  return a + b
}
console.log(add(1, 2))

副作用处理

有些代码虽然没被引用,但有副作用,不能删除:

// side-effect.js
export const value = 1

// 副作用:修改了全局对象
window.globalFlag = true

// 副作用:立即执行
console.log('module loaded')

通过 package.json 标记无副作用的模块:

{
  "name": "my-library",
  "sideEffects": false
}

或指定有副作用的文件:

{
  "sideEffects": [
    "*.css",
    "./src/polyfill.js"
  ]
}

影响 Tree Shaking 的写法

// ❌ 不利于 Tree Shaking - 导出整个对象
const utils = {
  add(a, b) { return a + b },
  subtract(a, b) { return a - b }
}
export default utils

// ✅ 有利于 Tree Shaking - 具名导出
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }
// ❌ 不利于 Tree Shaking - 解构 default
import utils from './utils'
utils.add(1, 2)

// ✅ 有利于 Tree Shaking - 具名导入
import { add } from './utils'
add(1, 2)

Dead Code Elimination

Tree Shaking 是 DCE(Dead Code Elimination)的一种实现。DCE 还包括:

// 1. 不可达代码
function foo() {
  return 1
  console.log('永远不会执行') // 会被删除
}

// 2. 无用赋值
let x = 1
x = 2  // 第一次赋值会被删除

// 3. 条件恒为假
if (false) {
  console.log('永远不会执行') // 会被删除
}

关键点

  • 静态分析:ES Module 的 import/export 在编译时确定,CommonJS 在运行时确定
  • 具名导出:使用 export { } 而非 export default 对象,便于分析
  • 副作用标记:通过 sideEffects 字段告诉打包工具哪些模块可以安全删除
  • DCE:Tree Shaking 是死代码消除的一种,专门处理模块级别的未使用导出
  • 工具支持:Webpack、Rollup、esbuild 等打包工具都支持 Tree Shaking