Tree-shaking 是现代前端构建工具中用于消除死代码(未被使用的代码)的重要优化手段,核心原理是利用 ES6 模块的静态特性(import/export)分析代码依赖,剔除未被引用的导出内容。
一、Tree-shaking 核心前提
Tree-shaking 仅对 ES6 模块(ESM) 有效,因为 ESM 具有以下静态特性:
- 模块依赖关系在编译时确定(而非运行时)。
import/export语句只能出现在模块顶层,无法动态修改。
CommonJS 模块(require/module.exports)因依赖关系动态化,无法被 Tree-shaking 优化。
二、Rollup 中的 Tree-shaking
Rollup 是专注于 ES 模块打包的工具,Tree-shaking 是其原生核心功能,实现简洁且高效。
1. 实现原理
Rollup 通过以下步骤实现 Tree-shaking:
- 依赖解析 :遍历模块的
import/export语句,构建模块依赖图。 - 标记未使用代码 :从入口模块出发,追踪所有被引用的导出(
export),未被引用的导出被标记为 "死代码"。 - 删除死代码:在打包阶段直接剔除标记的死代码,不生成冗余内容。
Rollup 对代码的静态分析更彻底,且默认输出未被混淆的代码,因此 Tree-shaking 效果直观可见。
2. 代码示例与效果
源文件
javascript
// utils.js(ES 模块)
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b; // 未被使用的函数
// index.js(入口文件)
import { add } from './utils.js';
console.log(add(1, 2)); // 仅使用 add
Rollup 配置(rollup.config.js)
arduino
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'es' // 输出 ES 模块(保留 import/export,便于观察)
}
};
打包结果(dist/bundle.js)
css
const add = (a, b) => a + b;
console.log(add(1, 2));
- 效果 :未被使用的
minus函数被完全剔除,输出代码简洁。
三、Webpack 中的 Tree-shaking
Webpack 从 v2 开始支持 Tree-shaking,但实现逻辑更复杂,受多种因素影响(如模式、压缩工具等)。
1. 实现原理
Webpack 的 Tree-shaking 分为三个阶段:
-
标记阶段:
- 解析 ES 模块的
import/export,通过ModuleConcatenationPlugin分析模块依赖。 - 对未被引用的导出标记为
/* unused harmony export xxx */(仅标记,不删除)。
- 解析 ES 模块的
-
删除阶段:
- 依赖 压缩工具(如 Terser) 剔除标记的死代码。
- 仅在
production模式下默认启用(开发模式为保留代码不删除)。
-
副作用处理:
- 通过
package.json的sideEffects字段标记模块是否有副作用(如全局变量修改、DOM 操作),避免误删有副作用的代码。
- 通过
2. 代码示例与效果
源文件(同 Rollup 示例)
javascript
// utils.js
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b; // 未被使用
// index.js
import { add } from './utils.js';
console.log(add(1, 2));
Webpack 配置(webpack.config.js)
css
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'production' // 必须为 production 模式才会删除死代码
};
打包结果(dist/bundle.js,简化后)
arduino
console.log(3); // add(1,2) 被直接计算,minus 完全删除
- 效果 :未被使用的
minus被剔除,且add函数因逻辑简单被进一步优化(直接计算结果)。
3. 关键配置:sideEffects
如果模块存在副作用(如修改全局变量),需在 package.json 中声明,避免被误删:
json
json
{
"sideEffects": [
"./src/polyfill.js", // 有副作用的模块
"*.css" // CSS 文件通常有副作用(插入样式)
]
}
sideEffects: false:表示所有模块均无副作用,可安全删除未引用代码。
四、核心差异对比
| 特性 | Rollup | Webpack |
|---|---|---|
| 分析能力 | 原生支持 ESM 静态分析,更彻底 | 需依赖插件(ModuleConcatenationPlugin),分析逻辑较复杂 |
| 删除死代码时机 | 打包阶段直接删除 | 依赖压缩工具(Terser)在优化阶段删除 |
| 开发模式效果 | 即使开发模式也会删除死代码 | 仅 production 模式删除(开发模式保留用于调试) |
| 输出可读性 | 输出代码接近源码,Tree-shaking 效果直观 | 输出代码经压缩混淆,效果需反推 |
| 适用场景 | 库文件打包(如工具库、组件库) | 应用程序打包(依赖复杂、需处理多种模块类型) |
五、总结
- Rollup:Tree-shaking 实现简洁高效,输出代码干净,适合库文件打包(如 React、Vue 等)。
- Webpack:Tree-shaking 需配合压缩工具和模式配置,功能更全面(支持多种模块类型、复杂依赖),适合应用程序打包。
六、直观点
- 整个过程就像 "清理房间":先摸清房间里的物品(依赖解析)→ 标记没用的垃圾(标记死代码)→ 直接扔掉垃圾(删除死代码);
- 对比 Webpack :Rollup 是 "边打包边扔垃圾",Webpack 是 "先标记垃圾,最后找清洁工(Terser)扔",所以 Rollup 的产物更直观看到剔除效果。