什么是 Tree Shaking?它的工作原理是什么?
在 Webpack 中,Tree Shaking(树摇)是一个非常关键的性能优化技术。
简单来说,Tree Shaking 就是“消除无用代码”(Dead Code Elimination)的代名词。
一、 什么是 Tree Shaking?
如果你把你的应用程序比作一棵“树”:
- 源代码和依赖库是这棵树的“树枝和绿叶”;
- 没有用到的代码(比如声明了但从未调用的函数、导入了但未使用的模块)就是“枯黄的死叶”;
- Tree Shaking 的过程就像是摇晃这棵树,把那些死掉的叶子摇掉,最终只打包真正用到的代码,以此来大幅减小最终生成的 JS 文件体积。
一个直观的例子:
假设你引入了一个工具库 utils.js:
// utils.js
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b; // 这个函数你根本没用过
在 main.js 中:
// main.js
import { add } from './utils.js';
console.log(add(1, 2));
开启 Tree Shaking 后,打出来的 bundle 文件中完全不会包含 minus 函数的代码。
二、 Tree Shaking 的工作原理
Tree Shaking 的实现依赖于 ES6 模块语法(ESM, import/export) 的静态特性,以及 Webpack 与代码压缩工具(如 Terser)的协作。它的工作流程主要分为以下几个步骤:
1. 静态代码分析(依赖 ESM 语法)
这是 Tree Shaking 能工作的前提。
- CommonJS(
require/module.exports)是动态的:你可以在if语句或者函数内部require一个模块,只有在运行时才知道导入了什么。 - ESM(
import/export)是静态的:import只能在模块的顶部,导入/导出关系在编译期(代码运行前)就能确定。
Webpack 在构建“依赖图谱(Dependency Graph)”时,通过 AST(抽象语法树)解析,能够精准地知道哪个模块输出了什么,以及哪个模块引入了什么。
2. Webpack 标记无用代码(Marking)
在编译阶段,Webpack 会遍历所有代码:
- 它会追踪每一个
export出来的变量或函数是否在其他地方被import和使用。 - 如果发现某个
export未被使用,Webpack 不会立刻删除它,而是在编译后的代码中加入特殊标记。 - 例如,Webpack 会在未使用的导出前加上注释:
/* unused harmony export minus */。
3. 代码压缩工具删除代码(Elimination)
Webpack 本身主要负责标记,真正删除死代码的是代码压缩工具(如 Terser、UglifyJS 或 ESBuild)。
当 Webpack 完成标记后,压缩工具会在生产模式(mode: 'production')下运行。压缩工具看到这些标记以及上下文后,确认这些代码不会被执行且没有副作用,就会直接从代码树中将它们剔除。
4. sideEffects(副作用)标记优化
有些代码虽然没有导出任何东西,但它一被加载就会产生影响(例如修改了全局变量、给 Array.prototype 挂载了方法、或者引入了全局 CSS)。这种行为叫做副作用(Side Effects)。
为了避免误删这些有副作用的代码,Webpack 支持在 package.json 中配置 sideEffects 属性:
- 如果设置
"sideEffects": false:告诉 Webpack “这个项目/库里的文件都是纯净的,如果没有被import具名导出,可以直接跳过整个文件”。 - 如果有副作用文件(如 CSS):可以设置数组形式
"sideEffects": ["*.css", "*.scss"]。
三、 如何在 Webpack 中开启 Tree Shaking?
在 Webpack 5 中,Tree Shaking 默认开启得非常好,但你需要确保以下几点:
- 使用 ESM 规范编写代码:确保使用
import和export。 - 避免 Babel 将 ESM 转化为 CommonJS:
在.babelrc或babel.config.js中设置modules: false:json{ "presets": [ ["@babel/preset-env", { "modules": false }] ] } - 设置模式为生产模式:
在webpack.config.js中设置mode: 'production',这会自动触发代码压缩和死代码删除。 - 在
package.json中配置sideEffects:
正确声明项目或依赖包是否有副作用。
四、 为什么有时候 Tree Shaking 会失效?
- 使用了 CommonJS 语法:如果用
require引入模块,Webpack 无法进行静态分析。 - Babel 把 ESM 编译成了 CommonJS(最常见坑点)。
- 代码含有副作用(Side Effects):如果 Webpack 认为某段代码有副作用,为了不破坏程序逻辑,它会保守地保留这段代码。
- 引入了整个大而全的库:例如
import _ from 'lodash'。lodash 官方发布的是 CommonJS 规范,无法 Tree Shaking;应该使用lodash-es或单独引入import cloneDeep from 'lodash/cloneDeep'。