
webpack Tree Shaking 机制实战解析从 harmony-unused 示例看 ES6 导入导出使用追踪与未用代码剔除【免费下载链接】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本指南以仓库自带的 harmony-unused 示例 为主线讲解 webpack 如何追踪 ES6import/export的实际使用情况并据此剔除 bundle 中未使用的导出——即常说的 tree shaking。文中会对比该示例在开发模式与生产模式下生成的产物深入 lib 层源码说明“使用信息如何跨模块流动”并给出可本地复现的验证命令帮助读者真正看懂 tree shaking 从“标记导出可用/已用”到“压缩器删除死代码”的完整链路。一、示例概览一个刻意“留白”的模块组harmony-unused示例位于 examples/harmony-unused共由 5 个文件组成模拟了现实中“入口文件 → 功能模块 → 大型库的聚合出口 → 被整体转发的子模块”这一经典结构文件角色example.js应用入口真正触发import使用math.js提供add/multiply/list三个导出library.js模拟大型库的“门户文件”只做 re-export 转发abc.js被library.js整体转发、但几乎没人用的子模块webpack.config.js最小化构建配置仅关闭模块串联以便观察说明template.md 是该示例的文档源文件其中使用_{{example.js}}_、_{{production:stdout}}_等占位符由 buildAll.js、template-common.js 等示例脚本配合真实打包生成最终版 README.md。也就是说文档里贴出的dist/output.js产物并非手写而是真实运行 webpack 后的结果。示例的 webpack.config.js 内容极少use strict; /** type {import(webpack).Configuration} */ const config { // mode: development || production, optimization: { concatenateModules: false } }; module.exports config;它刻意关闭了optimization.concatenateModules作用域提升/模块串联。这样生产构建后“哪些模块被保留、哪些模块被整块剔除”会以独立模块为单位清晰可见而不会被 Scope Hoisting 折叠到一个大函数里掩盖真相。二、逐文件解读谁在使用谁1. 入口 example.js两种典型的导入方式import { add } from ./math; import * as library from ./library; add(1, 2); library.reexportedMultiply(1, 2);这里同时展示了两种 ES6 导入写法命名导入import { add } from ./math随后直接调用add(1, 2)命名空间导入import * as library from ./library随后通过属性访问library.reexportedMultiply(1, 2)。可见实际“被使用”的只有math.add和经过library转发的multiply。2. math.js三个导出只被用到两个export function add() { var sum 0, i 0, args arguments, l args.length; while (i l) { sum args[i]; } return sum; } export function multiply() { var product 1, i 0, args arguments, l args.length; while (i l) { product * args[i]; } return product; } export function list() { return Array.from(arguments); }使用情况add—— 被入口直接使用multiply—— 经library.jsre-export 后被入口使用list——无人使用且其函数体内调用Array.from。原文档给出了一条极佳的“肉眼验证”线索在压缩后的 bundle 中若找不到Array.from即证明list已被剔除。3. library.js典型的“大库门户文件”export { a, b, c } from ./abc; export { add as reexportedAdd, multiply as reexportedMultiply } from ./math;library.js模拟了大型库常见的入口/索引模块它不包含任何自身实现只是把子模块的导出统一转发出去。现实中的 UI 组件库、工具库往往都有这样的“巨型 index”而使用者通常只用到其中一小部分导出。这个示例要演示的正是使用信息如何从example.js一路流经library.js流向abc.js。在本例中5 个转发出去的导出里只有reexportedMultiply真正被用到a/b/c/reexportedAdd全部落空。4. abc.js被整体转发、却被整体“蒸发”的子模块export function a() { console.log(a); } export function b() { console.log(b); } export function c() { console.log(c); }由于入口从未使用a/b/c中任何一个webpack 判定abc.js没有任何“已使用导出”于是该模块被整块排除出 bundle。原文档同样给出肉眼验证线索在压缩产物中找不到console.log(a)即证明abc.js完全不存在了。5. 使用关系一览example.js ├─ import { add } from ./math │ └─ add(1,2) → math.add 使用 ✓ ├─ import * as library from ./library │ └─ library.reexportedMultiply(1,2) │ └─ 转发自 ./math.multiply 使用 ✓ │ ├─ reexportedAdd转发 add 未使用 ✗ │ ├─ a / b / c转发自 ./abc.js 未使用 ✗ → abc.js 整体剔除 └─ math.list 未使用 ✗ → 声明被删除三、产物对比6.83 KiB 如何缩到 545 B示例文档贴出了两版dist/output.js恰好对应 tree shaking 的两级效果。1. 未优化开发模式风格产物全量保留未优化版本体积 6.83 KiB包含完整 3 个业务模块与 3 个 runtime 模块。其中的模块注释值得仔细读/* 1 */ ./math.js /*! namespace exports */ /*! export add [provided] [no usage info] [missing usage info prevents renaming] */ /*! export list [provided] [no usage info] [missing usage info prevents renaming] */ /*! export multiply [provided] [no usage info] [missing usage info prevents renaming] */math.js、library.js、abc.js全部原样输出连导出描述符都完整无缺。原因在于导出是否“provided”可用由静态分析得出但“使用信息”usage info此时缺失因此每个导出都被保守保留且因“missing usage info”而无法安全改名。stats 中入口模块标注为[used exports unknown]正是对这一状态的直白描述。对应的模块结构README.md 完整产物大致为./math.js模块 id 1保留add、list、multiply三个函数及全部导出描述符./library.js模块 id 2通过__webpack_require__.d声明 5 个 re-export./abc.js模块 id 3a/b/c函数及描述符齐全末尾是模块缓存、__webpack_require__.d定义导出 getter、__webpack_require__.o、__webpack_require__.rnamespace 标记等 runtime 代码。2. 生产模式产物只留下“被用到的”生产模式产物[minimized]仅 545 字节核心代码如下((){use strict;var r{627(r,t,e){function o(){for(var r0,t0,earguments,oe.length;to;)re[t];return r}function n(){for(var r1,t0,earguments,oe.length;to;)r*e[t];return e.d(t,{WQ:()o,lw:()n})}}};const t{};function e(o){const nt[o];if(void 0!n)return n.exports;const ct[o]{exports:{}};return ro,c.exports}e.d(r,t){for(var o in t)e.o(t,o)!e.o(r,o)Object.defineProperty(r,o,{enumerable:!0,get:t[o]})},e.o(r,t)Object.prototype.hasOwnProperty.call(r,t);var oe(627);(0,o.WQ)(1,2),o.lw(1,2)})();可以清楚验证原文档的两条观察线索Array.from消失list()因无人使用导出描述符未生成、声明也被后续压缩删除console.log(a)消失abc.js没有任何导出被使用整个模块连同library.js中转层不再出现在 bundle 中被保留的两个函数被压缩器重命名为o/n作为导出名则被改写成WQ/lw对应add与multiplylibrary.js的 re-export 转发层被彻底“消解”入口代码最终直接引用 math 模块命名空间上的导出。3. stats 信息对比项目UnoptimizedProduction mode产物output.js 6.83 KiB [emitted]output.js 545 bytes [emitted] [minimized]业务依赖模块3 modules584 bytes1 module347 bytesruntime 模块3 modules614 bytes2 modules403 bytes入口标注[no exports]/[used exports unknown][no exports]/[no exports used][used exports unknown]与[no exports used]之间的差异正是“是否做过使用分析”在 stats 层的直观体现。四、原理剖析webpack 如何知道哪些导出被使用文档开头点明本示例的演示对象webpack 追踪 ES6 导入/导出的使用仅把被使用的导出放进产物再由压缩器删除不再被引用的声明。这套机制在源码层面由两条关键流水线承担。1. 判定“提供哪些导出”FlagDependencyExportsPlugin第一个问题是“模块到底导出了什么”。该职责由 FlagDependencyExportsPlugin.js 承担它在模块解析完成后遍历模块图为每个模块记录导出集合及其来源直接声明、re-export、CommonJS 动态导出等。开发与生产模式下它都默认开启对应的配置开关是optimization.providedExports。在 schemas/WebpackOptions.json 中其描述为Figure out which exports are provided by modules to generate more efficient code.开发产物的模块注释里export add [provided]与other exports [not provided]就是该阶段产出信息的快照——前者表示可静态确认的具名导出后者表示存在无法静态列举的其他导出形式。2. 判定“哪些导出被使用”FlagDependencyUsagePlugin第二个问题是“谁真正用到了这些导出”由 FlagDependencyUsagePlugin.js 负责。它从入口出发沿着依赖图把“被使用”的状态逐模块传播、累加并把结果写入每个导出对应的ExportInfo。从 WebpackOptionsApply.js 可以看到两者在优化流水线中的注册次序if (options.optimization.sideEffects) { new SideEffectsFlagPlugin(...).apply(compiler); } if (options.optimization.providedExports) { new FlagDependencyExportsPlugin().apply(compiler); } if (options.optimization.usedExports) { const FlagDependencyUsagePlugin require(./FlagDependencyUsagePlugin); new FlagDependencyUsagePlugin( options.optimization.usedExports global, // Keep an escaping modules exports mangleable when mangling is on. Boolean(options.optimization.mangleExports) ).apply(compiler); }由此可以提炼出三个关键点先“提供”后“使用”必须先把每个模块的导出摸清楚providedExports 阶段后续“使用标记”才能落到具体的导出对象上使用标记的粒度FlagDependencyUsagePlugin内部借助 ExportsInfo.js 导出的UsageState枚举工作const UsageState Object.freeze({ Unused: 0, OnlyPropertiesUsed: 1, NoInfo: 2, Unknown: 3, Used: 4 });其中NoInfo开发构建里的[no usage info]代表尚未分析或无法分析webpack 只能保守保留一切只有状态推进到Used或部分属性的OnlyPropertiesUsed后其余导出才被允许省略“使用信息流动”的具体含义当入口访问library.reexportedMultiply时webpack 沿 re-export 链解析到math.js的multiply于是在 ExportsInfo.js 中以_redirectTo等方式建立“library.reexportedMultiply → math.multiply”的别名关联把Used状态沿着依赖方向持续扩散到真正定义该导出的模块。这正是原文档所说“usage information flows from example.js through library.js into abc.js”的机制本质——abc.js之所以整个消失是因为没有任何一个Used标记经过library.js落到它的a/b/c上。3. 删除动作发生在哪两步分工值得强调的是 tree shaking 在 webpack 中实际上是“两步分工、缺一不可”webpack 侧只负责“不带”代码生成阶段导出描述符只包含“被使用”的导出彻底没有已用导出的模块如abc.js直接不进 bundle。此时模块源码中未被导出的函数声明如list其实仍存在于源代码里webpack 并不会逐条改写函数体压缩器负责“删掉”生产模式默认开启optimization.minimize在 terser-webpack-plugin 等 minimizer 环节这些已失去导出描述符引用的顶层函数声明被判定为死代码并删除。于是Array.from、console.log(a)才会从最终产物中彻底消失。这一点在原文档开篇即有总结也与“导出描述符省略 压缩器 DCE”的两级产物未优化 6.83 KiB / 生产 545 B完全吻合。4. 相关优化配置与默认值速查从 lib/config/defaults.js 可以看到这些优化项的默认取值随 mode 变化配置项默认值作用optimization.providedExports恒为true静态分析模块提供哪些导出optimization.usedExports仅production下为true分析导出使用情况省略未用导出并允许改名可设为global跨 runtime 全局分析optimization.sideEffectsdevelopment 为flagproduction 为true标记无副作用模块在导出未被使用时跳过整个模块optimization.concatenateModules仅production下为true作用域提升本示例为便于观察而关闭optimization.minimize仅production下为true交给 minimizer 删除死代码/压缩关于各选项的官方语义可对照 schemas/WebpackOptions.json 中providedExports、sideEffects、usedExports的 description 字段例如usedExports支持true表示“按每个 runtime 分别分析已用导出”或global表示“对所有 runtime 合并做全局分析”。需要特别区分usedExports与sideEffectsusedExports是导出级的裁剪作用于模块内部的具名导出本例的核心演示对象sideEffects是模块级的跳过当模块package.json声明sideEffects: false或源码被判定无副作用时即使模块被 import 但导出未被使用整个模块也可以不加载。二者结合才构成现代 webpack 的完整 tree shaking 图景。五、本地复现与验证方法若想亲手跑一遍该示例并验证上述观察可按仓库的示例约定在examples/harmony-unused目录下以example.js为入口、输出到dist/output.js对应 README 中{{dist/output.js}}与{{production:dist/output.js}}两个产物位执行# 开发模式等价 README 的 Unoptimized 产物 webpack example.js -o dist --mode development # 生产模式等价 README 的 Production mode 产物 webpack example.js -o dist --mode production前提已在仓库根目录安装依赖参见 package.json / yarn.lock使webpackCLI 可用示例自身的 webpack.config.js 只提供concatenateModules: false入口与输出目录通过 CLI 参数给定。注意模式切换后产物会互相覆盖建议分目录存放或先后比对。随后即可用 grep 验证“删除证据”# 生产产物中不应再出现 list 与 Array.from对应 math.js 的 list 被剔除 grep -c Array.from dist/output.js # 生产产物中不应再出现 abc.js 的任何痕迹console.log(a) 应消失 grep -c console.log dist/output.js两者在生产产物中的结果都应为 0。若改成--mode development重新构建Array.from与console.log(a)又会回来——这就是“使用信息缺失时保守保留”的直观体现。六、从示例到工程实践可以借鉴什么用“门户文件”大量转发导出时务必确认下游只 import 需要的命名。library.js → abc.js的剔除效果证明re-export 链并不会阻止 tree shaking前提是链上的每个环节都能被静态解析且没有console.log之类的副作用遮蔽判断留意命名空间导入与属性访问的可分析性。本示例中import * as library 固定属性访问library.reexportedMultiply仍能被精确追踪到使用目标但若对命名空间做动态访问如library[key]或整体遍历使用状态会退化为Unknownwebpack 将被迫保守保留把“开发慢、生产小”的默认策略吃透。usedExports默认只在 production 开启是为了换取开发构建速度与源码可读性生产环境的体积优化应依赖mode: production所附带的一整套默认优化见 lib/config/defaults.js想深入学习可对照测试用例。仓库中 test/configCases/ 下的side-effects、cjs-tree-shaking等目录以及 test/cases/ 中大量 harmony 相关用例都围绕同一套导出分析与剔除机制展开可进一步印证本文所讲的FlagDependencyExportsPlugin/FlagDependencyUsagePlugin行为边界。小结harmony-unused示例用不到 10 个函数的体量完整呈现了 webpack tree shaking 的核心链路静态“提供导出”分析 沿模块图传播的“使用”标记 代码生成时省略未用导出 压缩器删除失去引用的声明。对比 6.83 KiB 与 545 B 两份产物配合Array.from、console.log(a)两个查找线索读者可以在几分钟内形成对 ES6 tree shaking 的直观且准确的认知再结合 lib/FlagDependencyUsagePlugin.js、lib/ExportsInfo.js、lib/WebpackOptionsApply.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),仅供参考