返回
创建于
状态
公开

简单来说:CommonJS (CJS) 基本上不支持 Tree-shaking,或者说效果非常有限。

Tree-shaking 的核心依赖于 ES Modules (ESM) 的“静态结构”特性,而 CommonJS 的“动态性”则是它的天然克星。


为什么 CommonJS 难以进行 Tree-shaking?

1. 动态导出(Dynamic Export)

在 CJS 中,你可以根据条件来决定导出什么。打包工具在不运行代码的情况下,很难确定 module.exports 最终会长什么样。

javascript
1if (process.env.NODE_ENV === 'production') {
2  module.exports = { a: 1 };
3} else {
4  module.exports = { b: 2 };
5}

2. 动态导入(Dynamic Require)

CJS 的 require 是同步且动态的。路径甚至可以是一个变量,这让打包工具无法在构建阶段(Static Analysis)建立完整的依赖图。

javascript
1const moduleName = 'utils';
2const tools = require(`./${moduleName}.js`); // 工具无法预知加载了哪个文件

3. 对象引用特性

CJS 导出的是一个对象。当你 require 一个模块时,你拿到的是这个对象的引用。现代打包工具很难追踪这个对象上的哪些属性没被用到,尤其是当对象被重新赋值或传递时。


现状:虽然难,但并非完全不可能

尽管原生不支持,但现代构建工具(如 Webpack, Rollup, Terser)做了一些尝试来优化 CJS:

  • Webpack 的优化:Webpack 会尝试分析简单的 CJS 导出(例如静态定义的 module.exports = { ... }),并在某些情况下通过 Terser 这种压缩工具识别并删除未引用的代码。但这非常依赖代码的写法是否足够“规整”。
  • 插件转换:有些开发者会使用 Babel 插件将 CJS 转换为 ESM,然后再进行 Tree-shaking。
  • Package.json 的 sideEffects:通过在 package.json 中标记 sideEffects: false,可以帮助工具更大胆地剔除未使用的模块,无论它是 CJS 还是 ESM。

对比总结

特性CommonJS (CJS)ES Modules (ESM)
加载方式运行时加载(动态)编译时加载(静态)
导出内容值的拷贝(对象)值的动态绑定(只读引用)
Tree-shaking极难/不支持天然支持
主要用途Node.js 服务端浏览器端、现代前端项目

建议

如果你希望项目拥有更好的性能和更小的体积:

  1. 优先使用 importexport (ESM)
  2. 在发布 npm 包时,提供 module 字段(指向 ESM 版本)和 main 字段(指向 CJS 版本)。
  3. 如果必须使用 CJS 库,尽量寻找支持按路径引入的库(例如 import lodash/fp 而不是 import { fp } from 'lodash')。