JavaScript 模块打包工具 Tree Shaking 原理
Tree Shaking 是现代 JavaScript 打包工具(如 Webpack、Rollup、Vite)用来移除未使用代码的核心技术。它的目标是在最终打包产物中只保留实际被引用的代码,从而减小文件体积、提升加载速度。
1. Tree Shaking 起作用的前提条件
确保你的项目满足以下两个基本条件,否则 Tree Shaking 无法生效:
-
使用 ES 模块(ESM)语法
必须使用import和export,而不是 CommonJS 的require()和module.exports。因为 ESM 是静态的——在代码运行前就能确定哪些导出被使用了;而 CommonJS 是动态的,打包工具无法在编译时判断哪些代码会被用到。 -
启用生产模式或显式开启代码优化
开发环境下通常不会进行 Tree Shaking,因为它会增加构建时间。只有在生产构建(如webpack --mode=production)时,打包器才会执行死代码消除(Dead Code Elimination)。
2. Tree Shaking 的工作原理
Tree Shaking 的本质是基于静态分析的死代码消除。整个过程分为三步:
-
解析所有模块依赖关系
打包工具从入口文件开始,递归分析所有import语句,构建完整的依赖图(Dependency Graph)。每个模块的导出(export)和导入(import)都会被记录。 -
标记被使用的导出项
从入口点出发,追踪哪些变量、函数或类被实际引用。例如:// math.js export const add = (a, b) => a + b; export const subtract = (a, b) => a - b;// main.js import { add } from './math.js'; console.log(add(1, 2));此时只有
add被标记为“使用”,subtract未被引用。 -
删除未被标记的代码
在生成最终 bundle 时,打包工具会跳过未被标记的导出项。上述例子中,subtract函数不会出现在输出文件里。
3. 如何验证 Tree Shaking 是否生效
手动检查打包结果是最直接的方式:
-
编写一个包含多个导出的测试模块
创建utils.js:export const usedFn = () => 'I am used'; export const unusedFn = () => 'I am NOT used'; export const alsoUnused = 'dead code'; -
在入口文件中只导入部分导出
index.js:import { usedFn } from './utils.js'; usedFn(); -
使用打包工具进行生产构建
以 Webpack 为例,运行:npx webpack --mode=production -
查看输出文件(通常是
dist/main.js)
搜索'I am NOT used'或alsoUnused。如果 Tree Shaking 生效,这些字符串不会出现在最终代码中。
注意:某些情况下,即使未使用,代码仍可能被保留。常见原因见下文“常见陷阱”。
4. 常见导致 Tree Shaking 失效的陷阱
即使满足 ESM 和生产模式,以下情况也会阻止 Tree Shaking:
陷阱一:使用命名空间导入(Namespace Import)
import * as utils from './utils.js';
utils.usedFn(); // ❌ 这会导致整个模块被保留
解决方法:改用具名导入:
import { usedFn } from './utils.js'; // ✅ 只引入需要的部分
陷阱二:副作用(Side Effects)
如果模块中有顶层代码(不在函数内),例如:
// logger.js
console.log('Logger loaded!'); // 副作用
export const log = msg => console.log(msg);
打包工具默认认为这类模块有副作用,即使 log 未被使用,也不会删除整个文件。
解决方法:在 package.json 中声明无副作用:
{
"sideEffects": false
}
或指定哪些文件有副作用:
{
"sideEffects": ["./src/polyfills.js", "*.css"]
}
陷阱三:第三方库未提供 ES 模块版本
很多 npm 包只提供 CommonJS 格式(如 lodash 主包)。此时即使你写 import { debounce } from 'lodash',实际导入的是整个库。
解决方法:使用支持 Tree Shaking 的子路径导入:
import debounce from 'lodash/debounce'; // ✅ 直接导入单个函数
或使用 lodash-es(ESM 版本):
import { debounce } from 'lodash-es'; // ✅ 支持 Tree Shaking
5. 不同打包工具的 Tree Shaking 行为对比
虽然原理相同,但各工具实现细节略有差异:
| 工具 | 默认开启 Tree Shaking | 对 CommonJS 支持 | 副作用处理方式 |
|---|---|---|---|
| Webpack | 仅在 mode=production |
有限(需插件) | 依赖 sideEffects 字段 |
| Rollup | 总是开启 | 不支持 | 自动检测,可配置 treeshake 选项 |
| Vite | 基于 Rollup,总是开启 | 不支持 | 同 Rollup |
Vite 在开发模式下也利用原生 ESM 实现按需加载,但真正的代码删除只发生在生产构建阶段。
6. 最佳实践:最大化 Tree Shaking 效果
遵循以下规则,确保你的代码能被有效摇树:
-
始终使用具名导入/导出
避免import * as和默认导出包含多个成员的对象。 -
将功能拆分为细粒度模块
每个文件只导出一个或少数几个相关函数,避免“工具箱”式大文件。 -
为项目设置
"sideEffects": false
如果确认所有代码无副作用,在package.json中明确声明。 -
优先选择提供 ESM 版本的依赖库
查看 npm 包是否包含module或exports字段指向.mjs文件。 -
定期审计打包体积
使用webpack-bundle-analyzer或rollup-plugin-visualizer生成可视化报告,检查是否有意外包含的大模块。
# Webpack 示例
npx webpack-bundle-analyzer dist/stats.json
通过以上步骤,你可以确保 Tree Shaking 充分发挥作用,交付最小化的 JavaScript 代码。

暂无评论,快来抢沙发吧!