Tree Shaking 依赖 ES Modules 的静态可分析性,因其 import/export 必须在顶层、命名明确,构建工具可精准建立引用图谱并剔除未使用导出;而 CommonJS 的 require/module.exports 是运行时动态的,导致 Tree Shaking 失效。

因为 ES Modules(ESM)的导入导出关系在编译时就能完全确定,而 Tree Shaking 正是依赖这种静态可分析性来识别并剔除未使用的代码。
ESM 是静态的,能被构建工具提前“看懂”
ESM 要求 import 和 export 必须写在模块顶层,不能出现在条件语句或函数内部。这使得打包工具(如 Webpack、Rollup、Vite)在构建阶段就能扫描源码,准确建立模块间的引用图谱。例如:
-
import { debounce } from 'lodash-es'→ 工具立刻知道只用了debounce这一个导出 -
export const a = 1; export function b() {}→ 每个导出标识符都显式命名,可单独标记是否被引用
CommonJS 是动态的,构建时无法判断哪些代码真没用
CommonJS 使用 require() 和 module.exports,它们是运行时执行的函数调用。导出对象可能被随时修改,比如:
if (Math.random() > 0.5) module.exports.foo = someFunc;exports[config.method] = handler;
这类写法让工具无法在构建阶段确认 foo 是否会被用到,只能保守保留整个模块,Tree Shaking 就失效了。
静态结构还支撑了更精细的优化逻辑
Tree Shaking 不只是删整个文件,而是能精确到单个函数、常量甚至对象属性。这依赖 ESM 的两个关键特性:
- 命名导出(named export):每个导出有固定名称,可独立追踪使用状态
- 无运行时绑定副作用:ESM 模块本身不执行逻辑(除非顶层语句),避免因“副作用”被整块保留
不满足这个前提,其他优化都难生效
即使开了生产模式、配了 sideEffects: false、用了 Terser 压缩,只要代码里混着 require 或 Babel 把 import 转成了 CommonJS,静态分析基础就崩了。Tree Shaking 就会退化成普通压缩,无法安全剔除未引用的导出。











