为何Webpack对含潜在副作用模块的打包结果存在差异?
我有两段近乎相同的代码,但使用Webpack构建时,生成的bundle存在关键差异:一种情况模块被包含在bundle中,另一种情况则被排除。原本以为Webpack会将所有带有潜在副作用的模块都包含在bundle中,无论条件逻辑如何,但实际情况并非总是如此,以下是具体场景和原因解析。
模块被包含在bundle中的情况
// index.js const flag = false; if (flag) { require("./myModule"); }
打包后的dist/bundle.js保留了模块结构:
(() => { var e = { 584: (e, r, t) => { "use strict"; function o() { return "myFunction"; } t.r(r), t.d(r, {myFunction: () => o}); }, }, r = {}; function t(o) { var n = r[o]; if (void 0 !== n) return n.exports; var i = (r[o] = {exports: {}}); return e[o](i, i.exports, t), i.exports; } (t.d = (e, r) => { for (var o in r) t.o(r, o) && !t.o(e, o) && Object.defineProperty(e, o, {enumerable: !0, get: r[o]}); }), (t.o = (e, r) => Object.prototype.hasOwnProperty.call(e, r)), (t.r = (e) => { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {value: "Module"}), Object.defineProperty(e, "__esModule", {value: !0}); }); })();
模块被排除在bundle外的情况
// index.js if (false) { require("./myModule"); }
打包后的dist/bundle.js为空:
// It became an empty file.
项目配置与结构
目录结构
myProject ├── src │ ├── index.js │ └── myModule.js ├── dist ├── webpack.config.js └── package.json
Webpack配置
// webpack.config.js const path = require("path"); module.exports = { mode: "production", entry: "./src/index.js", output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, optimization: { usedExports: true, }, };
package.json
{ "name": "myProject", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "webpack": "^5.95.0", "webpack-cli": "^5.1.4" } }
被引入的模块
// myModule.js export function myFunction() { return "myFunction"; }
核心原因解析
Webpack在生产模式下默认启用死代码消除(Tree Shaking),但能否消除代码完全取决于编译期的静态分析结果:
if (false)场景:
Webpack能直接识别这个分支是编译期可确定的死代码,永远不会被执行。哪怕模块可能存在副作用,Webpack也能判定require调用永远不会触发,因此直接删除整个分支及模块引用,最终bundle为空。const flag = false; if (flag)场景:
这里的flag虽然是常量,但Webpack的静态分析没有将其值直接折叠到if条件中。编译器会认为这个变量的取值在理论上存在变动可能(即使实际是固定值),因此无法确定该分支是死代码。
加上开启了usedExports: true,Webpack会标记模块导出是否被使用,但因为require所在分支无法被判定为死代码,Webpack会保留模块的包裹结构(尽管导出的myFunction未被使用),所以bundle中会留下模块的框架代码,却没有实际执行逻辑。
如果想让const flag = false场景的代码也被消除,可以尝试开启optimization.concatenateModules(模块串联优化),或者用/*#__PURE__*/标记require调用,明确告诉Webpack该调用没有副作用,这样即使条件无法确定,也能被安全消除。
内容的提问来源于stack exchange,提问作者Miyaboom

