文章目录
- 前言
- [一、什么是 Tree Shaking](#一、什么是 Tree Shaking)
-
- [1.1 定义](#1.1 定义)
- [1.2 为什么叫"摇树"](#1.2 为什么叫"摇树")
- [二、ESM 静态分析](#二、ESM 静态分析)
-
- [2.1 ESM 的静态特性](#2.1 ESM 的静态特性)
- [2.2 为什么 ESM 能 Tree Shaking](#2.2 为什么 ESM 能 Tree Shaking)
- [三、CJS 为何难摇树](#三、CJS 为何难摇树)
-
- [3.1 CommonJS 的动态特性](#3.1 CommonJS 的动态特性)
- [3.2 CJS 的 Tree Shaking 限制](#3.2 CJS 的 Tree Shaking 限制)
- [3.3 解决方案](#3.3 解决方案)
- [四、sideEffects 配置](#四、sideEffects 配置)
-
- [4.1 定义](#4.1 定义)
- [4.2 什么是副作用](#4.2 什么是副作用)
- [4.3 副作用标记错误会怎样](#4.3 副作用标记错误会怎样)
- [五、Webpack 配置](#五、Webpack 配置)
-
- [5.1 启用 Tree Shaking](#5.1 启用 Tree Shaking)
- [5.2 两步过程](#5.2 两步过程)
- 六、易混淆点
- 七、思考与练习
- 总结
前言
Tree Shaking 是现代构建工具的核心优化技术,通过静态分析移除未使用的代码,减小打包体积。本篇会讲清楚:
- Tree Shaking 的原理
- ESM 静态分析的优势
sideEffects配置- CJS 为何难摇树
一、什么是 Tree Shaking
1.1 定义
Tree Shaking 是指通过静态分析,移除项目中未被引用的代码(Dead Code),从而减小最终打包体积。
javascript
// utils.js
export function add(a, b) {
return a + b
}
export function subtract(a, b) {
return a - b
}
// main.js
import { add } from './utils'
console.log(add(1, 2))
// Tree Shaking 后:subtract 函数被移除
1.2 为什么叫"摇树"
这个术语来自 ES6 模块的静态结构特性------就像摇树一样,把没有被引用的"枯叶"(未使用的导出)摇落。
二、ESM 静态分析
2.1 ESM 的静态特性
ES6 模块(ESM)的 import / export 是静态声明,在编译时就能确定依赖关系。
javascript
// ✅ ESM:静态导入,编译时确定
import { add } from './utils'
// ❌ 不能这样写(动态导入无法静态分析)
if (condition) {
import { add } from './utils'
}
// ✅ 动态导入(但这是按需加载,不是 Tree Shaking)
const module = await import('./utils')
2.2 为什么 ESM 能 Tree Shaking
javascript
// ESM 的 import 是声明式的
import { add } from './utils'
// Webpack 可以在编译时分析出:
// 1. utils.js 导出了 add 和 subtract
// 2. main.js 只使用了 add
// 3. 因此可以安全移除 subtract
三、CJS 为何难摇树
3.1 CommonJS 的动态特性
CommonJS(CJS)的 require 是动态执行的,无法在编译时确定依赖关系。
javascript
// CJS:require 是函数调用,运行时执行
const utils = require('./utils')
// 无法在编译时确定:
// 1. utils 是否被使用
// 2. utils 的哪些属性被使用
// 3. require 的路径是否是动态的
// 动态路径
const moduleName = getModuleName()
const module = require(moduleName)
3.2 CJS 的 Tree Shaking 限制
javascript
// CJS 导出
module.exports = {
add: function(a, b) { return a + b },
subtract: function(a, b) { return a - b }
}
// CJS 导入
const utils = require('./utils')
console.log(utils.add(1, 2))
// 问题:无法确定 utils.subtract 是否被使用
// 因为 utils 是一个对象,属性访问是动态的
3.3 解决方案
javascript
// 方案 1:使用 ESM 语法
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }
// 方案 2:使用具名导出(而不是 default 导出整个对象)
// ❌ 不利于 Tree Shaking
export default { add, subtract }
// ✅ 有利于 Tree Shaking
export { add, subtract }
四、sideEffects 配置
4.1 定义
sideEffects 告诉 Webpack 哪些文件有副作用,哪些没有。没有副作用的文件如果未被使用,可以安全移除。
javascript
// package.json
{
"sideEffects": false // 所有文件都没有副作用
}
// 或指定有副作用的文件
{
"sideEffects": [
"*.css",
"*.polyfill.js"
]
}
4.2 什么是副作用
javascript
// 有副作用的代码
let globalVar = 0
export function increment() {
globalVar++
return globalVar
}
// 这个模块修改了全局变量,不能被移除
// 即使 increment 函数没有被导入使用
// 无副作用的代码
export function add(a, b) {
return a + b
}
// 这个模块是纯函数,可以安全移除
4.3 副作用标记错误会怎样
javascript
// 错误:将有副作用的模块标记为无副作用
{
"sideEffects": false
}
// 代码:
import './polyfill' // 修改了全局对象
import { add } from './utils'
// 结果:polyfill 被错误移除,导致兼容性问题
五、Webpack 配置
5.1 启用 Tree Shaking
javascript
// webpack.config.js
module.exports = {
mode: 'production', // 生产模式自动启用 Tree Shaking
optimization: {
usedExports: true, // 标记未使用的导出
minimize: true // 压缩时移除未使用的代码
}
}
5.2 两步过程
1. usedExports:标记未使用的导出
- 分析模块图
- 标记哪些导出被使用,哪些没有
2. minimize(压缩):移除未使用的代码
- Terser 等压缩工具识别标记
- 移除未使用的导出
六、易混淆点
- Tree Shaking 只对 ESM 有效 :CJS 的
require是动态执行的,无法在编译时静态分析。 sideEffects: false的含义:告诉 Webpack 所有模块都没有副作用,未使用的模块可以安全移除。- 副作用标记错误:将有副作用的模块标记为无副作用,会导致全局修改、polyfill 等被错误移除。
- production 模式:Webpack 的 production 模式自动启用 Tree Shaking,无需手动配置。
- default 导出不利于 Tree Shaking :
export default { add, subtract }无法分析单个函数是否被使用。
七、思考与练习
1. 为什么 ESM 可以 Tree Shaking 而 CJS 不行?
解析:
- ESM :
import/export是静态声明,编译时确定依赖关系 - CJS :
require是函数调用,运行时执行,无法静态分析
2. 什么是副作用?sideEffects: false 的作用是什么?
解析:
- 副作用:模块执行时会影响全局状态(如修改全局变量、设置原型链等)
- sideEffects: false:告诉 Webpack 所有模块都没有副作用,未使用的模块可以安全移除
3. 错误标记 sideEffects 会有什么后果?
解析:将有副作用的模块标记为无副作用,会导致:
- polyfill 被移除,兼容性问题
- 全局修改被移除,功能异常
- 样式文件被移除,页面样式丢失
4. 如何让 CJS 模块也能 Tree Shaking?
解析:
- 方案 1:改用 ESM 语法
- 方案 2:使用具名导出(而不是 default 导出整个对象)
- 方案 3:使用工具将 CJS 转换为 ESM
5. Tree Shaking 的两步过程是什么?
解析:
- usedExports:标记未使用的导出(分析模块图)
- minimize:压缩时移除未使用的代码(Terser 识别标记)
6. export default { add, subtract } 为什么不利于 Tree Shaking?
解析:default 导出是一个整体对象,Webpack 无法分析单个属性(add、subtract)是否被使用。应该使用具名导出:
javascript
// ❌ 不利于 Tree Shaking
export default { add, subtract }
// ✅ 有利于 Tree Shaking
export { add, subtract }
总结
- Tree Shaking 通过静态分析移除未使用的代码,减小打包体积
- ESM 静态特性 :
import/export是声明式的,编译时确定依赖 - CJS 动态特性 :
require是函数调用,无法静态分析 - sideEffects:标记有副作用的文件,避免错误移除
- 两步过程 :
usedExports标记 +minimize移除