Webpack 对 CommonJS 模块的 Tree Shaking:机制、配置与产物深度解析

发布时间:2026/9/6 20:31:25
Webpack 对 CommonJS 模块的 Tree Shaking:机制、配置与产物深度解析 Webpack 对 CommonJS 模块的 Tree Shaking机制、配置与产物深度解析【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack本文基于 webpack 仓库中的官方示例 examples/cjs-tree-shaking/README.md完整还原webpack 如何对 CommonJSrequire/exports模块执行 tree shaking这一主题从入口源码、optimization配置到产物中未使用导出被重命名、被置入__webpack_unused_export__占位变量的全过程并结合仓库源码说明usedExports与mangleExports的默认行为及实现位置帮助读者理解 CommonJS 模块在打包后的死代码消除边界。一、示例能解决什么问题通常大家认为 tree shaking 只对 ES Module 生效因为 CommonJS 的require是运行时行为、导出对象也可能被动态修改。webpack 对此做了静态分析只要 CommonJS 模块的导出方式和导入方式都落在它能静态识别的模式集合内它就能像处理 ESM 一样标记哪些导出被使用used exports、哪些未被使用并在mangleExports开启时把未使用的导出重命名成短名字方便压缩器后续真正删除它们。本示例用最简单的三个文件入口 两个 CommonJS 依赖演示了这一点并给出开启 tree shaking与关闭 tree shaking两份产物做对照。二、示例源码三种可识别的导入写法入口文件 examples/cjs-tree-shaking/example.js 覆盖了 webpack 能静态识别的三种 CommonJS 导入模式// Property access pattern属性访问模式 const inc require(./increment).increment; var a 1; inc(a); // 2 // Destructuring assignment pattern解构赋值模式 const { add } require(./math); add(a, 2); // 3 // Aliased destructuring带别名的解构 const { increment: inc2 } require(./increment); inc2(a); // 2两个依赖模块均为纯exports.xxx function风格的 CommonJS 模块examples/cjs-tree-shaking/increment.js导出increment、incrementBy2、decrement三个函数内部通过const add require(./math).add;引用 math 模块examples/cjs-tree-shaking/math.js导出add与multiply两个函数注意multiply内部还有一处历史遗留的sum变量笔误但本示例只关注导出使用情况不影响分析。入口实际只使用了increment.increment和math.add因此math.multiply、increment.incrementBy2、increment.decrement三个导出应当被判为 unused。三、webpack 配置usedExports 与 mangleExports 的对照实验examples/cjs-tree-shaking/webpack.config.js 导出两个编译配置形成对照use strict; /** type {import(webpack).Configuration[]} */ const config [ { entry: ./example.js, output: { pathinfo: true, // 在产物中输出模块/导出信息注释 filename: output.js }, optimization: { moduleIds: size, usedExports: true, // 分析哪些导出被使用 mangleExports: true // 把未使用的导出重命名为短名 } }, { entry: ./example.js, output: { pathinfo: true, filename: without.js // 关闭 tree shaking 的对照产物 }, optimization: { moduleIds: size, usedExports: false, mangleExports: false } } ]; module.exports config;要点pathinfo: true会在产物中生成/*! export add [provided] [used in main] ... */这类注释是理解分析结果的关键第一个配置开启usedExportsmangleExports产物为dist/output.js第二个配置显式关闭两者产物为dist/without.js用于展示不做导出分析时的产物形态。关于默认值从源码 lib/config/defaults.js 可以看到D(optimization, usedExports, production); D(optimization, mangleExports, production);即usedExports与mangleExports的默认值等于mode production。因此生产模式下这两项默认开启这也是 README 中Production mode小节产物更小、且被压缩的原因开发模式下默认关闭除非手动配置这也是为什么示例要显式写出这两个选项。四、开启 tree shaking 的产物分析dist/output.jsREADME 展示的开发态产物未压缩保留 pathinfo 注释中两个依赖模块的导出状态一目了然/* 0 */ /*!*****************!*\ !*** ./math.js ***! \*****************/ /*! default exports */ /*! export add [provided] [used in main] [usage prevents renaming] */ /*! export multiply [provided] [unused] [renamed to l] */ /*! runtime requirements: __webpack_exports__ */ /***/ ((__unused_webpack_module, exports) { var __webpack_unused_export__; exports.add function add() { /* ...原样保留... */ }; __webpack_unused_export__ function multiply() { /* ...原样保留... */ }; /***/ }), /* 1 */ /*! default exports */ /*! export decrement [provided] [unused] [renamed to K] */ /*! export increment [provided] [used in main] [usage prevents renaming] */ /*! export incrementBy2 [provided] [unused] [renamed to B] */ /***/ ((__unused_webpack_module, exports, __webpack_require__) { var __webpack_unused_export__; const add (__webpack_require__(/*! ./math */ 0).add); exports.increment function increment(val) { return add(val, 1); }; __webpack_unused_export__ function incrementBy2(val) { return add(val, 2); }; __webpack_unused_export__ function decrement(val) { return add(val, 1); }; /***/ })这里发生了三件事导出状态标注注释格式为export 名字 [provided] [used/unused] [renamed to 短名]。[usage prevents renaming]表示该导出被入口main chunk实际使用名字不能动[unused] [renamed to l/K/B]表示未使用且已被 mangle 成单字母名。未使用导出被架空exports.multiply ...被改写为__webpack_unused_export__ ...。源码仍然保留因为 CommonJS 的副作用无法在开发态断定但它不再挂到exports对象上且所有未使用导出共享同一个变量名__webpack_unused_export__——这正是为压缩器做的铺垫Terser 等 minifier 能识别出该变量从未被读取从而把整个赋值连同函数体一并删除。入口侧同样被静态化dist/output.js中的入口代码保留了原始注释与结构const inc (__webpack_require__(1).increment)等三种导入模式全部被解析为具体导出的依赖而不是对整个模块的黑盒依赖。五、生产模式产物真正删掉了未使用的代码README 的 dist/output.js (production) 小节给出压缩后的产物447 字节/*! For license information please see output.js.LICENSE.txt */ ((){var n[(n,t){t.addfunction(){for(var n0,t0,rarguments,er.length;te;)nr[t];return n}},(n,t,r){const er(0).add;t.incrementfunction(n){return e(n,1)}}];const t{};function r(e){...}(0,r(1).increment)(1);const{add:e}r(0);e(1,2);const{increment:o}r(1);o(1)})();对比源码可以发现math.js只剩addmultiply整个函数体消失了increment.js只剩incrementincrementBy2、decrement也消失了。这就是第 4 节中__webpack_unused_export__占位变量存在的意义开发态只负责改名 脱钩生产态的 minifier 负责删除两步配合完成了 CommonJS 模块的死代码消除。六、对照组关闭 tree shaking 的产物dist/without.jsdist/without.js (same without tree shaking) 小节给出关闭usedExports/mangleExports后的压缩产物615 字节((){var n[(n,t){t.addfunction(){...},t.multiplyfunction(){...}},(n,t,r){const er(0).add;t.incrementfunction(n){return e(n,1)},t.incrementBy2function(n){return e(n,2)},t.decrementfunction(n){return e(n,1)}}];...差异一目了然t.multiply、t.incrementBy2、t.decrement全部保留体积 615 bytes vs 开启后的 447 bytes。README 的 Info 小节同时给出了两种模式下的 stats 输出其中最能说明问题的行是入口模块下的导出状态标注# Unoptimized 模式 asset output.js 3.18 KiB [emitted] (name: main) # 开启 usedExports asset without.js 3.32 KiB [emitted] (name: main) # 关闭时略大 ./example.js 277 bytes [built] [code generated] [no exports used] # output.js入口无对外导出明确无导出被使用 [used exports unknown] # without.js无法判断哪些导出被使用[no exports used]与[used exports unknown]的对照正是usedExports分析生效与否在 stats 层面的直接体现未开启时 webpack 对导出使用情况一无所知unknown自然不可能做重命名更谈不上压缩删除。七、从源码看这套机制在 webpack 内部的实现以上产物并非魔法对应到仓库中的实现可以分为三层1. 静态解析层CommonJS 语法被翻译成导出依赖lib/dependencies/CommonJsExportsParserPlugin.js解析exports.xxx ...、module.exports.xxx ...、Object.defineProperty(exports, ...)等导出语句记录本模块提供了哪些导出lib/dependencies/CommonJsImportsParserPlugin.js解析require(x).xxx、const { xxx } require(x)等导入语句记录本模块使用了哪些导出lib/dependencies/CommonJsPlugin.js将上述解析器注册到 webpack 的解析流程中并提供相应的依赖与模板处理。examples/cjs-tree-shaking/cases.txt 是这个示例配套的模式清单明确列出 webpack 期望识别的四类写法及反例BAD无法识别会破坏分析module.exports abc; module.exports.xxx abc;、exports abc;、裸用module.exports/exports/this、function f() { return this; }等EXPORTS可识别的导出exports.xxx abc;、module.exports { ... }; module.exports.xxx abc;等IMPORT可识别的导入require(x).xxx、var { xxx } require(x);、var x require(x); x.xxx;REEXPORT可识别的再导出module.exports.xxx require(x).xxx;、var xxx require(x); module.exports { xxx: xxx.xxx };等TRANSPILEDTypeScript 的__export(m)与 Babel 的_interopRequireDefault辅助函数生成的代码也在支持范围内——这说明经 TS/Babel 转译出的 CommonJS 同样有机会享受 tree shaking。这份清单同时划出了边界一旦模块中出现整对象重新赋值后再接着挂属性这类动态模式webpack 就无法静态确定导出集合分析会退化为 unknown对应 stats 里的[used exports unknown]tree shaking 也就无从谈起。2. 标记层判定 provided / used / unusedlib/optimize/FlagDependencyExportsPlugin.js在usedExports开启时执行为每个模块的导出打上[provided]由该模块提供标记lib/optimize/FlagDependencyUsagePlugin.js根据各依赖import实际读取了哪些导出反向标记消费方模块中的导出为[used in main]或[unused]。产物注释里[export add [provided] [used in main]]的双标记正是这两个插件协作的结果。3. 改写层mangle 与防重命名lib/optimize/MangleExportsPlugin.js在mangleExports开启时把未被任何 chunk 使用的导出重命名为l、K、B这类短名对应产物注释中的[renamed to l]保证多个未使用导出可以折叠成同一个变量lib/ConstPlugin.js负责把exports.multiply fn这类赋值改写成__webpack_unused_export__ fn的 ConstDependency 替换完成脱钩。MangleExportsPlugin 中还有一个值得注意的限制lib/optimize/MangleExportsPlugin.jsoptimization.mangleExports cant be used with cacheUnaffected as export mangling is a global effect即mangleExports是跨 chunk 的全局效果与某些增量缓存场景不兼容——在需要仅对受影响模块重编译的高级缓存配置中这是一个真实的取舍点。此外lib/config/defaults.js 中还有D(splitChunks, usedExports, optimization.usedExports true)说明optimization.usedExports会进一步传递给splitChunks用于按导出使用情况拆分公共 chunk——tree shaking 的标记是整个优化管线共享的数据而非孤立功能。八、实践结论结合本示例的产物与 stats 对照可以得出几条可直接使用的结论CommonJS 模块也能被 tree shaking但有前提导出与导入都必须落在 cases.txt 所列的静态模式内exports.xxx ...require(x).xxx/ 解构导入module.exports整体动态重写等写法会使分析退化为 unknown。生产模式默认开启usedExports与mangleExports默认值跟随production见 lib/config/defaults.js因此mode: production下无需额外配置即可生效开发模式想要同样的分析注释需要显式配置这两项并建议配合output.pathinfo: true如 webpack.config.js 所示来观察导出状态。删除是两步走的webpack 负责标记 unused、重命名并脱钩到__webpack_unused_export__真正的代码删除由 minifier 完成。因此开启 tree shaking 但产物没变小时应检查 minify 是否启用以及模块是否含有动态写法。验证手段stats 中入口模块的[no exports used]vs[used exports unknown]、产物 pathinfo 注释中的[used in main]/[unused] [renamed to ...]是判断 tree shaking 是否对目标模块生效的最直接依据本示例的对照组447 bytes vs 615 bytes 的压缩产物也给出了量级参考。转译代码同样受益TypeScript/Babel 输出的 CommonJS 辅助函数模式__export、_interopRequireDefault在支持清单内现代工程链路中的 JS/TS 混用不必放弃 CommonJS 侧的 tree shaking。如需复现可按 examples/cjs-tree-shaking/README.md 的流程执行该示例目录下的构建脚本node examples/buildAll.js cjs-tree-shaking或按 examples/README.md 说明构建单个示例即可得到上述dist/output.js与dist/without.js两份对照产物。【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考