Tree Shaking原理,为什么es6写法可以,commonjs写法不可以

291 阅读3分钟

Tree Shaking 是一种通过静态分析代码依赖关系,移除未使用代码的优化技术。核心目标是减少最终打包后的代码体积,只保留实际被用到的部分。然后说下为什么 ES6 模块支持 Tree Shaking,而 CommonJS 不支持。


Tree Shaking 的原理

  1. 静态分析
    打包工具(如 Webpack、Rollup)在构建代码时,会分析模块之间的导入(import)和导出(export)关系。它会构建一棵“依赖树”,确定哪些代码被实际使用,哪些从未被引用。

  2. 标记未使用代码
    工具通过 ES6 模块的静态结构,明确哪些导出(函数、变量等)未被其他模块导入,并将它们标记为“未使用”。

  3. 移除死代码
    在生成最终代码时,工具会删除所有被标记为未使用的代码,只保留真正需要的部分。


为什么 ES6 模块支持 Tree Shaking?

ES6 模块的设计是 静态的,这意味着:

  1. 导入/导出必须在顶层作用域
    例如,importexport 不能写在 if 条件、函数内部或运行时逻辑中。

    // ✅ ES6 静态导入(支持 Tree Shaking)
    import { add } from './math.js';
    
    // ❌ 以下写法会报错(无法动态导入)
    if (condition) {
      import { add } from './math.js'; // 语法错误!
    }
    
  2. 模块路径必须是静态字符串
    导入路径不能是变量,只能是字符串字面量:

    // ✅ 合法
    import './math.js';
    
    // ❌ 非法
    const path = 'math.js';
    import(path); // 报错!
    
  3. 依赖关系在构建时就能确定
    由于 ES6 模块的静态特性,打包工具在构建阶段就能分析出所有依赖关系,明确哪些代码未被使用,从而安全地移除它们。


为什么 CommonJS 不支持 Tree Shaking?

CommonJS(Node.js 的模块系统)是 动态的,这导致:

  1. require() 可以出现在任何地方
    require 可以在函数、条件语句或运行时逻辑中动态调用:

    // CommonJS 动态导入(无法 Tree Shaking)
    if (condition) {
      const math = require('./math'); // 运行时才能确定是否加载
      console.log(math.add(1, 2));
    }
    
  2. 模块路径可以是动态生成的
    路径可以是变量,甚至通过计算得到:

    const moduleName = 'math';
    const math = require(`./${moduleName}`); // 打包工具无法静态分析!
    
  3. 导出内容可以动态修改
    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 的重要原因之一。