Tree Shaking 是一种通过静态分析代码依赖关系,移除未使用代码的优化技术。核心目标是减少最终打包后的代码体积,只保留实际被用到的部分。然后说下为什么 ES6 模块支持 Tree Shaking,而 CommonJS 不支持。
Tree Shaking 的原理
-
静态分析
打包工具(如 Webpack、Rollup)在构建代码时,会分析模块之间的导入(import)和导出(export)关系。它会构建一棵“依赖树”,确定哪些代码被实际使用,哪些从未被引用。 -
标记未使用代码
工具通过 ES6 模块的静态结构,明确哪些导出(函数、变量等)未被其他模块导入,并将它们标记为“未使用”。 -
移除死代码
在生成最终代码时,工具会删除所有被标记为未使用的代码,只保留真正需要的部分。
为什么 ES6 模块支持 Tree Shaking?
ES6 模块的设计是 静态的,这意味着:
-
导入/导出必须在顶层作用域
例如,import和export不能写在if条件、函数内部或运行时逻辑中。// ✅ ES6 静态导入(支持 Tree Shaking) import { add } from './math.js'; // ❌ 以下写法会报错(无法动态导入) if (condition) { import { add } from './math.js'; // 语法错误! } -
模块路径必须是静态字符串
导入路径不能是变量,只能是字符串字面量:// ✅ 合法 import './math.js'; // ❌ 非法 const path = 'math.js'; import(path); // 报错! -
依赖关系在构建时就能确定
由于 ES6 模块的静态特性,打包工具在构建阶段就能分析出所有依赖关系,明确哪些代码未被使用,从而安全地移除它们。
为什么 CommonJS 不支持 Tree Shaking?
CommonJS(Node.js 的模块系统)是 动态的,这导致:
-
require()可以出现在任何地方
require可以在函数、条件语句或运行时逻辑中动态调用:// CommonJS 动态导入(无法 Tree Shaking) if (condition) { const math = require('./math'); // 运行时才能确定是否加载 console.log(math.add(1, 2)); } -
模块路径可以是动态生成的
路径可以是变量,甚至通过计算得到:const moduleName = 'math'; const math = require(`./${moduleName}`); // 打包工具无法静态分析! -
导出内容可以动态修改
CommonJS 允许在运行时修改导出对象:// math.js exports.add = (a, b) => a + b; setTimeout(() => { exports.subtract = (a, b) => a - b; // 动态添加导出! }, 1000);
由于这些动态特性,打包工具无法在构建阶段确定代码的依赖关系,因此 无法安全地移除未使用的代码。
结论
- ES6 模块:静态结构让工具在构建时就能分析依赖关系,实现 Tree Shaking。
- CommonJS:动态特性导致依赖关系只能在运行时确定,无法静态分析,因此不支持 Tree Shaking。
如果想优化代码体积,尽量使用 ES6 模块语法(import/export),并确保打包工具(如 Webpack、Rollup)的配置支持 Tree Shaking。对于 CommonJS 模块(如某些老旧库),Tree Shaking 几乎无法生效,这也是现代生态逐渐转向 ES6 的重要原因之一。