面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777
。

Tree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,通过静态分析模块间的导入导出关系,精确识别并移除未被引用的代码,从而显著减小最终打包体积并提升应用性能。这一技术最初由 Rich Harris 在 Rollup 中创新性地实现,随后 Webpack 在 2.0 版本开始支持,如今已成为现代前端工程中不可或缺的优化手段。Tree-Shaking 的成功依赖于 ES Module 的静态结构特性,使构建工具能够在编译阶段确定代码的使用情况,这也是它区别于传统 CommonJS 模块优化的关键所在。
在 Webpack 中启动 Tree Shaking
在 Webpack 中启用 Tree Shaking 需要满足以下条件:
-
使用 ES Module 语法(import/export
),而非 CommonJS(require)
-
将 Webpack 模式设置为 production:
module.exports = {
mode: "production",
};
-
在 optimization 选项中启用 usedExports:
module.exports = {
mode: "production",
optimization: {
usedExports: true,
},
};
-
在 package.json 中添加 "sideEffects" 属性,标记哪些文件有副作用:
{
"name": "moment",
"sideEffects": false
}
或指定有副作用的文件:
{
"name": "moment",
"sideEffects": ["*.css", "*.scss"]
}
-
确保你的 Babel 配置不会将 ES Modules 转换为 CommonJS 模块
编写代码时,应该避免导入整个库,而是只导入需要的部分:
// 不推荐(无法 tree shake)
import _ from "lodash";
// 推荐(可以 tree shake)
import { debounce } from "lodash";
为什么 Commonjs 不能实现 tree shaking
CommonJS 无法实现 Tree Shaking 的深层原因在于其动态特性与运行时行为:
CommonJS 模块系统允许高度动态的导入模式。开发者可以在条件语句中使用 require()
,支持变量路径导入,甚至允许在任何作用域中导入模块。例如:
if (process.env.NODE_ENV === "development") {
require("./debug-tools");
}
const moduleId = getModuleId();
const module = require(`./modules/${moduleId}`);
同时,CommonJS 采用值拷贝的导出方式,导出的是完整模块对象。即使只使用一个属性,也必须导入整个模块对象,无法在编译时确定使用了哪些具体导出项。依赖解析发生在执行阶段,模块关系图只有在代码实际运行时才能完全确定。静态分析工具无法可靠地预测所有可能的模块加载路径,动态 require
调用的结果依赖于运行时环境和条件。
相比之下,ES Modules
的设计从根本上解决了这些问题:
ES Modules
严格限制导入导出语句的位置和形式,所有 import
/export
必须位于模块顶层。下面的代码在 ESM 中是非法的:
if (condition) {
import { foo } from "./module"; // 语法错误!
export const bar = "bar"; // 语法错误!
}
这种限制使得模块依赖图在编译时完全确定。ES Modules
支持精确的导入导出关系,可以明确指定需要的导出项,构建工具能够创建精确的依赖关系图,确定哪些导出项实际被使用。
此外,ES Modules
的编译时可分析性也很关键。模块说明符必须是字符串字面量,编译器可以在不执行代码的情况下构建完整的模块依赖图,准确识别哪些导出项从未被任何模块引用。
因此,ES Modules
的这些特性为 Tree Shaking
提供了必要的静态分析基础,使构建工具能够准确识别并移除未使用的代码,从而大幅减小最终打包体积。
实现原理
在 Webpack 中,Tree-shaking 通过"标记-清除"两阶段实现未使用代码的消除。整个过程精确且高效:
首先,在编译阶段 Webpack 会构建完整的模块关系图并标记未使用的导出:
-
构建阶段(Make):分析模块代码,提取所有导出变量并记录到模块依赖图(ModuleGraph)结构中,建立模块间的依赖关系网络
-
封装阶段(Seal):遍历模块依赖图,确定每个模块的导出变量是否被其他模块引用,对未被引用的导出变量做标记
-
生成阶段:输出最终代码时,根据标记状态生成有效的导出语句,已被标记为"未使用"的导出在产物代码中会以注释形式存在
随后,在优化阶段,Terser 等压缩工具会识别这些标记并物理移除未使用的代码,最终生成精简的产物文件。
这种设计将"分析依赖"与"清除死代码"解耦,使 Webpack 能在不同环境中灵活应用 Tree-shaking 技术。
首先,标记功能需要配置 optimization.usedExports = true
开启,如下代码所示:
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
optimization: {
usedExports: true,
},
};
然后我们编写如下代码:

我们再 moment.js 文件导出了一个 moment 变量,并且在入口文件中使用了。
接下来我们将执行打包的操作,最终输出的结果如下所示:
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => {
// webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = {
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
eval(
'/* unused harmony exports bar, foo */\n/* harmony import */ var _moment__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moment */ "./src/moment.js");\nconst bar = "bar";\nconst foo = "foo";\n\n/* unused harmony default export */ var __WEBPACK_DEFAULT_EXPORT__ = ("foo-bar");\n\n\n\nconsole.log(_moment__WEBPACK_IMPORTED_MODULE_0__.moment);\n\n\n//# sourceURL=webpack://debug-example/./src/index.js?'
);
/***/
},
/***/ "./src/moment.js":
/*!***********************!*\
!*** ./src/moment.js ***!
\***********************/
/***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
eval(
'/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ moment: () => (/* binding */ moment)\n/* harmony export */ });\nconst moment = "moment";\n\n\n//# sourceURL=webpack://debug-example/./src/moment.js?'
);
/***/
},
/******/
};
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/
}
/******/ // Create a new module (and put it into the cache)
/******/ var module = (__webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {},
/******/
});
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/
}
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for (var key in definition) {
/******/ if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
/******/ Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
/******/
}
/******/
}
/******/
};
/******/
})();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
/******/
})();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/
/******/
})();
这段代码是 webpack 开发模式下的打包输出,清晰展示了 Tree Shaking 的"标记"阶段实现:
webpack 通过静态分析精确识别出模块中的未使用导出,并用特殊注释标记它们。在这个例子中:
这种标记机制让 webpack 在不影响代码功能的情况下,为后续的压缩阶段提供明确的"删除指南"——在开发模式下保留全部代码以便调试,而在生产模式下由 Terser 等工具移除这些已标记的未使用代码,从而减小最终包体积。
整个过程依赖于 webpack 精心设计的模块系统,其中两个关键组件发挥着核心作用:
__webpack_module_cache__
作为模块缓存仓库,存储已执行模块的结果,确保每个模块只被执行一次,无论它被引用多少次。这不仅提高了运行效率,也保证了模块状态的一致性,尤其对含有副作用的模块至关重要。
__webpack_require__
函数则模拟了模块加载器,它智能管理模块的加载、执行与缓存流程:首先检查模块是否已缓存,若已缓存则直接返回;否则创建新模块实例、执行模块代码并缓存结果。这个精密的加载机制使 webpack 能够在运行时准确还原静态分析所建立的模块依赖关系。
通过这套机制,webpack 不仅实现了代码模块化,更让 Tree Shaking 成为可能——它可以精确追踪哪些导出真正被使用,哪些仅仅是"死代码"。这个过程完美展示了 Tree Shaking 的工作原理:先标记,后清除,最终生成高效精简的代码包。
1. 收集模块导出
首先,Webpack 需要弄清楚每个模块分别有什么导出值,这一过程发生在 make 阶段,大体流程就是将模块的所有 ESM 导出语句转换为 Dependency 对象,并记录到 module 对象的 dependencies 集合,转换规则:
-
具名导出转换为 HarmonyExportSpecifierDependency
对象
-
default
导出转换为 HarmonyExportExpressionDependency
对象
如下代码所示:

对应的 dependencies
值为:

所有模块都编译完毕之后,触发 compilation.hooks.finishModules
钩子,开始执行 FlagDependencyExportsPlugin
插件回调。
FlagDependencyExportsPlugin 插件执行流程详解
FlagDependencyExportsPlugin
是 Webpack 中负责标记模块导出信息的核心插件,它在 Tree Shaking 的第一阶段(标记阶段)发挥关键作用。下面我会详细解释这个插件的执行流程,特别是关于缓存内容和导出规范的部分。
1. 插件注册和初始化
apply(compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
// 插件逻辑
});
}
插件通过 compiler.hooks.compilation.tap
注册到编译器的 compilation 钩子上,当 Webpack 创建新的 compilation 对象时会触发这个钩子。
2. 主要执行阶段
插件主要在 finishModules
钩子阶段执行,这个阶段是所有模块构建完成后、开始优化之前:
compilation.hooks.finishModules.tapAsync(PLUGIN_NAME, (modules, callback) => {
// 主要逻辑
});
3. 初始化阶段
const logger = compilation.getLogger(PLUGIN_LOGGER_NAME);
let statRestoredFromMemCache = 0;
// 其他统计变量...
const { moduleMemCaches } = compilation;
const queue = new Queue();
在初始化阶段,插件首先设置日志记录器并创建统计变量以跟踪缓存恢复情况,然后获取编译过程的内存缓存(moduleMemCaches),最后建立处理队列用于存放待分析的模块,为后续的导出信息收集和标记做好准备。
4. 尝试从缓存恢复导出信息
缓存中存储了什么?
缓存中存储的是模块的导出信息,具体包括:
-
模块的导出变量列表:模块中导出的所有变量名称
-
每个导出变量的提供状态(provided):表示该导出是否确实被提供
-
true
- 确定提供
-
false
- 确定不提供
-
null
- 不确定
-
导出变量的混淆可能性(canMangle):表示该导出名称是否可以在压缩时被混淆
-
导出变量的使用状态(used):表示该导出是否被其他模块使用
-
导出变量的重定向信息:如果导出是重定向到其他模块的导出,则包含目标信息
// 恢复缓存的示例代码
asyncLib.each(
modules,
(module, callback) => {
const exportsInfo = moduleGraph.getExportsInfo(module);
// 处理没有声明导出类型的模块
if (
(!module.buildMeta || !module.buildMeta.exportsType) &&
exportsInfo.otherExportsInfo.provided !== null
) {
// 对于没有明确声明导出类型的模块(如CommonJS模块),
// 假定它可能导出任何内容
statNoExports++;
exportsInfo.setHasProvideInfo();
exportsInfo.setUnknownExportsProvided();
return callback();
}
// 处理不可缓存的模块(没有hash值的模块)
if (typeof module.buildInfo.hash !== "string") {
statFlaggedUncached++;
queue.enqueue(module);
exportsInfo.setHasProvideInfo();
return callback();
}
// 首先尝试从内存缓存恢复(更快)
const memCache = moduleMemCaches && moduleMemCaches.get(module);
const memCacheValue = memCache && memCache.get(this);
if (memCacheValue !== undefined) {
statRestoredFromMemCache++;
// 从缓存恢复导出信息
exportsInfo.restoreProvided(memCacheValue);
return callback();
}
// 其次尝试从持久化缓存恢复
cache.get(module.identifier(), module.buildInfo.hash, (err, result) => {
if (err) return callback(err);
if (result !== undefined) {
statRestoredFromCache++;
// 从持久化缓存恢复导出信息
exportsInfo.restoreProvided(result);
} else {
// 没有缓存,需要重新分析
statNotCached++;
queue.enqueue(module);
exportsInfo.setHasProvideInfo();
}
callback();
});
}
// 继续下一阶段...
);
这段代码是 Webpack 中 Tree Shaking 功能的基础部分,它通过 FlagDependencyExportsPlugin
插件来分析和收集每个模块的导出信息。具体来说,它会遍历项目中的所有模块,对每个模块进行分析,确定该模块导出了哪些变量(如函数、类、常量等),以及这些导出是通过什么方式实现的(如直接导出、重命名导出、星号导出等)。
在处理每个模块时,代码采用了一个三层的处理策略:首先,对于那些使用 CommonJS(如 module.exports
)这样的模块,由于无法静态分析其导出内容,会直接将其标记为"可能导出任何内容";其次,对于之前处理过的模块,会尝试从内存缓存或磁盘缓存中恢复其导出信息,这样可以显著提升构建性能;最后,对于那些没有缓存信息的模块,则将其放入队列中,等待后续进行完整的代码分析。
这些收集到的导出信息会被存储在 Webpack 的模块图(ModuleGraph)中,成为后续 Tree Shaking 过程的重要依据。通过这些信息,Webpack 可以准确地知道每个模块提供了哪些导出,这些导出是否被其他模块使用,从而在最终打包时可以安全地移除那些未被使用的代码。
ModuleGraph 是 Webpack 内部的核心数据结构,负责追踪和管理所有模块间的依赖关系与导出信息。它通过 exportsInfo 系统精确记录每个模块"导出了什么"以及"这些导出如何被使用",为 Tree Shaking 优化提供关键决策依据。作为 Webpack 构建流程的中枢神经系统,ModuleGraph 使编译器能够理解代码间的内在联系,从而实现智能代码生成和高效打包优化。
如下图所示:

这种设计不仅确保了 Tree Shaking 的准确性,还通过多级缓存机制显著提升了构建性能,特别是在开发模式下的反复构建场景中。同时,它还能安全地处理不同类型的模块系统(ES Modules 和 CommonJS),保证了打包结果的正确性。
5. 分析导出信息 - 何为导出规范(ExportsSpec)?
**导出规范(ExportsSpec)**是描述模块如何导出变量的对象,它包含以下关键信息:
-
exports:可以是以下三种情况之一
-
canMangle:表示导出名称是否可以被混淆(重命名为短名称)
-
from:如果导出是重定向,指向源模块
-
priority:优先级,在多个模块提供相同导出名称时使用
-
terminalBinding:表示这是最终绑定,不应该被其他导出覆盖
-
hideExports:需要隐藏的导出列表
-
excludeExports:要排除的导出列表(当 exports 为 true 时使用)
// 导出规范的例子
{
exports: [
"default",
{ name: "helper", canMangle: true },
{ name: "utils", exports: ["format", "parse"] }
],
canMangle: true,
priority: 0
}
// 或者未知的所有导出
{
exports: true,
excludeExports: ["private"],
canMangle: false
}
canMangle 详解
canMangle
(可混淆)字面意思是指"是否可以改变/混淆该变量的名称":
-
为什么需要混淆名称:
-
何时不能混淆名称:
可以混淆的情况:
// module.js
export const calculateTotal = (a, b) => a + b;
// 导入时使用静态导入
import { calculateTotal } from "./module";
calculateTotal(1, 2);
这种情况下,calculateTotal
的 canMangle
为 true
,因为导入使用了静态命名导入,Webpack 可以安全地将 calculateTotal
重命名为更短的名称(如 a
):
// 压缩后可能变成这样
const a = (t, n) => t + n;
a(1, 2);
不能混淆的情况:
// library.js
export const VERSION = "1.0.0";
export const helper = { format: () => {} };
// 用户可能这样使用
import * as lib from "./library";
console.log(lib["VERSION"]); // 使用字符串访问
这种情况下,VERSION
的 canMangle
应为 false
,因为它可能被动态访问,如果重命名会破坏外部代码的正常工作。
canMangle 在 Tree Shaking 中的作用:
-
Webpack 中的 canMangle
标记用于控制代码压缩过程中的变量名重命名,被标记为 canMangle: true
的导出可以被压缩工具更激进地重命名,这不仅能减小代码体积,还能带来更多优化机会。
-
对于 canMangle: false
的导出,压缩工具会保留其原始名称,这主要是为了保证那些通过动态方式引用的代码能够正常工作。
-
Webpack 通过导出声明分析来确定 canMangle
状态,默认情况下,普通的导出都会被标记为 canMangle: true
。
-
不同类型的模块有不同的 canMangle
处理策略:ES 模块的导出通常可以被混淆,而 CommonJS 模块由于其动态特性,会更保守地设置为 canMangle: false
。
-
在库模式下(output.library
),Webpack 会将暴露的导出标记为 canMangle: false
,因为这些导出可能会被外部代码以各种方式引用。
-
对于包含副作用的模块,其导出通常会被标记为 canMangle: false
,这是为了避免重命名导致的潜在问题。
-
这种标记机制是 Tree Shaking 优化的重要组成部分,通过精确控制哪些变量名可以被重命名,在保证代码正确性的同时实现最大程度的代码压缩。
canMangle
状态是 Webpack 中决定导出变量名是否可以被压缩工具重命名的标志。它是 Tree Shaking 优化的重要组成部分,通过允许安全的名称重命名,帮助减小最终打包文件的大小。
当 Webpack 分析模块时,会为每个导出确定适当的 canMangle
状态,并将这些信息传递给后续的压缩工具,以实现安全且高效的代码优化。
6. 处理依赖块和依赖 - 收集导出规范
const processDependenciesBlock = (depBlock) => {
for (const dep of depBlock.dependencies) {
processDependency(dep);
}
for (const block of depBlock.blocks) {
processDependenciesBlock(block);
}
};
const processDependency = (dep) => {
// 从每个依赖中获取导出规范
const exportDesc = dep.getExports(moduleGraph);
if (!exportDesc) return;
exportsSpecsFromDependencies.set(dep, exportDesc);
};
这两个函数的目的是递归地遍历模块的所有依赖,并收集每个依赖提供的导出规范:
-
**依赖块(DependenciesBlock)**是包含依赖的容器,可以是:
-
**依赖(Dependency)**表示模块间的依赖关系,例如:
-
ImportDependency:对应 import { foo } from './bar'
-
CommonJsRequireDependency:对应 require('./foo')
-
HarmonyExportSpecifierDependency:对应 export const foo = 5

-
dep.getExports(moduleGraph):每种依赖类型都有自己的逻辑来提供导出规范
-
对于导入语句,它描述导入了哪些变量
-
对于导出语句,它描述导出了哪些变量
如下图所示:

7. 处理导出规范 - 核心逻辑详解
const processExportsSpec = (dep, exportDesc) => {
// 获取导出描述信息
const exports = exportDesc.exports;
const globalCanMangle = exportDesc.canMangle;
const globalFrom = exportDesc.from;
const globalPriority = exportDesc.priority;
const globalTerminalBinding = exportDesc.terminalBinding || false;
const exportDeps = exportDesc.dependencies;
// 处理隐藏的导出
if (exportDesc.hideExports) {
for (const name of exportDesc.hideExports) {
const exportInfo = exportsInfo.getExportInfo(name);
exportInfo.unsetTarget(dep);
}
}
// 处理未知导出情况 - 如 export * from './module'
if (exports === true) {
if (
exportsInfo.setUnknownExportsProvided(
globalCanMangle,
exportDesc.excludeExports,
globalFrom && dep,
globalFrom,
globalPriority
)
) {
changed = true;
}
}
// 处理具名导出列表
else if (Array.isArray(exports)) {
const mergeExports = (exportsInfo, exports) => {
for (const exportNameOrSpec of exports) {
// 解析导出规范详情
let name;
let canMangle = globalCanMangle;
let terminalBinding = globalTerminalBinding;
let exports;
let from = globalFrom;
let fromExport;
let priority = globalPriority;
let hidden = false;
// 处理字符串形式的导出名称
if (typeof exportNameOrSpec === "string") {
name = exportNameOrSpec;
}
// 处理对象形式的导出规范
else {
name = exportNameOrSpec.name;
if (exportNameOrSpec.canMangle !== undefined)
canMangle = exportNameOrSpec.canMangle;
if (exportNameOrSpec.export !== undefined)
fromExport = exportNameOrSpec.export;
if (exportNameOrSpec.exports !== undefined)
exports = exportNameOrSpec.exports;
if (exportNameOrSpec.from !== undefined) from = exportNameOrSpec.from;
if (exportNameOrSpec.priority !== undefined)
priority = exportNameOrSpec.priority;
if (exportNameOrSpec.terminalBinding !== undefined)
terminalBinding = exportNameOrSpec.terminalBinding;
if (exportNameOrSpec.hidden !== undefined)
hidden = exportNameOrSpec.hidden;
}
// 获取或创建导出信息对象
const exportInfo = exportsInfo.getExportInfo(name);
// 更新导出的提供状态
if (exportInfo.provided === false || exportInfo.provided === null) {
exportInfo.provided = true;
changed = true;
}
// 更新是否可以被混淆
if (exportInfo.canMangleProvide !== false && canMangle === false) {
exportInfo.canMangleProvide = false;
changed = true;
}
// 更新终端绑定状态
if (terminalBinding && !exportInfo.terminalBinding) {
exportInfo.terminalBinding = true;
changed = true;
}
// 处理嵌套导出(如对象中的属性)
if (exports) {
const nestedExportsInfo = exportInfo.createNestedExportsInfo();
// 递归处理嵌套导出
mergeExports(nestedExportsInfo, exports);
}
// 设置导出目标(对于re-export情况)
if (
from &&
(hidden
? exportInfo.unsetTarget(dep)
: exportInfo.setTarget(
dep,
from,
fromExport === undefined ? [name] : fromExport,
priority
))
) {
changed = true;
}
// 重新计算目标导出信息
const target = exportInfo.getTarget(moduleGraph);
let targetExportsInfo;
if (target) {
// 如果导出是重定向,获取目标导出信息
const targetModuleExportsInfo = moduleGraph.getExportsInfo(
target.module
);
targetExportsInfo = targetModuleExportsInfo.getNestedExportsInfo(
target.export
);
// 添加模块依赖关系
const set = dependencies.get(target.module);
if (set === undefined) {
dependencies.set(target.module, new Set([module]));
} else {
set.add(module);
}
}
// 更新导出信息中的重定向
if (exportInfo.exportsInfoOwned) {
if (exportInfo.exportsInfo.setRedirectNamedTo(targetExportsInfo)) {
changed = true;
}
} else if (exportInfo.exportsInfo !== targetExportsInfo) {
exportInfo.exportsInfo = targetExportsInfo;
changed = true;
}
}
};
// 开始处理导出列表
mergeExports(exportsInfo, exports);
}
// 处理依赖关系
if (exportDeps) {
cacheable = false;
for (const exportDependency of exportDeps) {
// 添加模块依赖关系
const set = dependencies.get(exportDependency);
if (set === undefined) {
dependencies.set(exportDependency, new Set([module]));
} else {
set.add(module);
}
}
}
};
这个函数的核心作用是根据收集到的导出规范,更新模块的导出信息:
-
处理不同类型的导出
-
为每个导出设置状态
-
处理导出重定向
-
设置导出目标(对于 re-export 情况)
-
建立导出信息与目标导出信息的连接
-
维护模块间的依赖关系图
-
处理嵌套导出:对于如 export const utils = { format, parse }
这样的嵌套导出,递归处理其内部结构
8. 通知依赖 - 级联更新
const notifyDependencies = () => {
const deps = dependencies.get(module);
if (deps !== undefined) {
for (const dep of deps) {
queue.enqueue(dep);
}
}
};
当一个模块的导出信息发生变化时,所有依赖于它的模块都需要重新分析,这个函数实现了级联更新:
-
从依赖图中查找依赖于当前模块的所有模块
-
将这些模块加入处理队列,以便在后续迭代中重新分析它们
-
这确保了导出信息的变化能正确地传播到整个依赖图
9. 缓存导出信息 - 为下次构建做准备
asyncLib.each(
modulesToStore,
(module, callback) => {
// 跳过不可缓存的模块
if (typeof module.buildInfo.hash !== "string") {
return callback();
}
// 获取要缓存的导出信息
const cachedData = moduleGraph
.getExportsInfo(module)
.getRestoreProvidedData();
// 更新内存缓存
const memCache = moduleMemCaches && moduleMemCaches.get(module);
if (memCache) {
memCache.set(this, cachedData);
}
// 存储到持久化缓存
cache.store(
module.identifier(),
module.buildInfo.hash,
cachedData,
callback
);
},
(err) => {
logger.timeEnd("store provided exports into cache");
callback(err);
}
);
这段代码实现了 Webpack 构建过程中的关键优化机制:将模块导出信息存储到缓存系统。通过这种方式,Webpack 能在后续构建中快速恢复模块的导出状态,避免重复分析,显著提升构建性能。
它有如下实现细节
-
缓存资格判断:首先检查模块是否具备缓存条件,通过验证 module.buildInfo.hash
是否为字符串。这个哈希值代表模块内容的指纹,只有带有有效哈希的模块才会进入缓存流程。
-
导出数据提取:对符合条件的模块,调用 moduleGraph.getExportsInfo(module).getRestoreProvidedData()
获取其导出信息。这些数据包含了模块所有导出的详细状态,是 Tree Shaking 优化的基础。
-
分层缓存策略
-
内存缓存:通过 memCache.set(this, cachedData)
将数据存入内存,优化同一构建周期内的重复访问
-
持久化缓存:通过 cache.store()
将数据持久保存,支持跨构建会话的信息复用
-
精确缓存标识:持久化缓存采用 module.identifier()
和 module.buildInfo.hash
作为键,确保缓存的精确匹配和失效控制。只有模块内容完全相同时才能命中缓存,保证了优化的安全性。
-
非阻塞执行模式:整个缓存过程通过 asyncLib.each
异步执行,防止在处理大型项目时阻塞主线程,提高了构建的响应性和效率。
这个缓存机制是 Webpack 增量构建和 Tree Shaking 优化的关键支撑,通过智能复用分析结果,大幅降低了重复构建的计算开销。
10. 模块重建支持 - 热更新的基础
const providedExportsCache = new WeakMap();
// 当模块开始重建时,保存其当前的导出信息
compilation.hooks.rebuildModule.tap(PLUGIN_NAME, (module) => {
providedExportsCache.set(
module,
moduleGraph.getExportsInfo(module).getRestoreProvidedData()
);
});
// 重建完成后,恢复之前的导出信息
compilation.hooks.finishRebuildingModule.tap(PLUGIN_NAME, (module) => {
moduleGraph
.getExportsInfo(module)
.restoreProvided(providedExportsCache.get(module));
});
这部分代码是为增量构建和热模块替换(HMR)设计的:
- 当模块需要重新构建时(如文件变化),先保存其当前的导出信息
- 模块重建完成后,恢复这些导出信息
- 这确保了即使模块重建,其导出信息也能保持一致,避免不必要的级联更新
导出规范的实际例子
为了更直观地理解导出规范,下面是几个具体的例子:
例子 1:具名导出
// module.js
export const foo = 123;
export function bar() {}
对应的导出规范:
{
exports: [
{ name: "foo", canMangle: true },
{ name: "bar", canMangle: true }
],
canMangle: true
}
例子 2:默认导出
// module.js
export default class MyClass {}
对应的导出规范:
{
exports: [
{ name: "default", canMangle: true }
],
canMangle: true
}
例子 3:重导出
// module.js
export { foo, bar as baz } from "./other-module";
对应的导出规范:
{
exports: [
{ name: "foo", from: "./other-module", export: ["foo"] },
{ name: "baz", from: "./other-module", export: ["bar"] }
],
canMangle: true
}
例子 4:星号导出
// module.js
export * from "./utils";
对应的导出规范:
{
exports: true,
from: "./utils",
canMangle: true
}
小结
FlagDependencyExportsPlugin
的执行流程可以归纳为以下步骤:
-
缓存恢复阶段:插件首先尝试从内存和持久化缓存中恢复模块的导出状态信息,避免不必要的重复分析,提高构建速度。
-
导出信息收集:对于缓存未命中的模块,插件深入分析其依赖图谱和导出结构,收集包含变量名称、混淆选项、优先级和重定向路径等关键数据的导出规范。
-
状态处理与更新:基于收集到的导出规范,插件更新模块的导出信息表,标记每个导出的提供状态、混淆可能性和目标引用,构建精确的导出映射。
-
依赖传播机制:当某个模块的导出信息变更时,插件自动将依赖于该模块的所有模块加入处理队列,确保导出状态变化能在整个依赖网络中正确传递。
-
智能缓存更新:分析完成后,插件将最新的导出状态信息同时写入内存和持久化缓存系统,优化后续构建过程。
该插件通过精确标记和追踪模块导出信息,为 Webpack 的 Tree Shaking 机制提供了关键的决策依据。它构建了一个详尽的"导出-使用"关系图,使 Webpack 能够准确识别并保留仅被实际引用的代码,从而显著减小最终打包体积。
经过 FlagDependencyExportsPlugin
插件处理后,所有 ESM 风格的 export 语句都会记录在 ModuleGraph
体系内,后续操作就可以从 ModuleGraph
中直接读取出模块的导出值。
最终 FlagDependencyExportsPlugin
插件的执行流程如下流程图所示:
┌──────────────────────────────────────────────────────────────────────┐
│ 注册到编译系统 │
│ compiler.hooks.compilation.tap │
└──────────────────────────────────┬───────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────────┐
│ 模块构建完成触发点 │
│ compilation.hooks.finishModules.tapAsync │
└──────────────────────────────────┬───────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────────┐
│ 初始化阶段 │
├──────────────────────────────────────────────────────────────────────┤
│ ▸ 创建模块处理队列 │
│ ▸ 设置统计变量 │
│ ▸ 准备缓存访问 │
└──────────────────────────────────┬───────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────────┐
│ 导出信息缓存恢复 │
├──────────────────────────────────────────────────────────────────────┤
│ ① 对每个模块执行缓存检查: │
│ ┌─────────┐ ┌────────────────────────┐ │
│ │无导出类型├───→│标记为提供未知导出 │ │
│ └─────────┘ └────────────────────────┘ │
│ │
│ ┌─────────┐ ┌────────────────────────┐ │
│ │不可缓存 ├───→│加入分析队列 │ │
│ └─────────┘ └────────────────────────┘ │
│ │
│ ┌─────────┐ ┌────────────────────────┐ │
│ │内存缓存 ├───→│恢复导出信息 │ │
│ │命中 │ │跳过后续分析 │ │
│ └─────────┘ └────────────────────────┘ │
│ │
│ ┌─────────┐ ┌────────────────────────┐ │
│ │持久缓存 ├───→│恢复导出信息 │ │
│ │命中 │ │跳过后续分析 │ │
│ └─────────┘ └────────────────────────┘ │
│ │
│ ┌─────────┐ ┌────────────────────────┐ │
│ │缓存未命中├───→│加入分析队列 │ │
│ └─────────┘ └────────────────────────┘ │
└──────────────────────────────────┬───────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────────┐
│ 模块分析循环 │
├──────────────────────────────────────────────────────────────────────┤
│ ① 从队列取出一个模块 │
│ ↓ │
│ ② 遍历模块依赖,收集导出规范 ──→ processDependenciesBlock │
│ ↓ │
│ ③ 处理每个依赖的导出规范 ──→ processExportsSpec │
│ • 处理隐藏导出 │
│ • 处理未知导出 (exports === true) │
│ • 处理具名导出列表 │
│ ↓ │
│ ④ 检测导出信息变化 ┌──────┐ │
│ │ 有变化 ├──→ 通知依赖此模块的模块重新分析 │
│ └──────┘ │
│ ↓ │
│ ⑤ 标记可缓存模块 │
│ ↓ │
│ ⑥ 重复执行直到队列为空 │
└──────────────────────────────────┬───────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────────┐
│ 导出信息缓存存储 │
├──────────────────────────────────────────────────────────────────────┤
│ ① 遍历所有可缓存模块 │
│ ↓ │
│ ② 提取模块导出信息 → getRestoreProvidedData() │
│ ↓ │
│ ③ 同时更新双层缓存: │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ 内存缓存 │ │ 持久化缓存 │ │
│ │ memCache.set │ │ cache.store │ │
│ └────────────────┘ └────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
2. 标记模块导出
FlagDependencyUsagePlugin
是 webpack 中 Tree Shaking 机制的"使用端"标记器,它与 FlagDependencyExportsPlugin
形成完整的标记系统:前者负责标记"模块提供了什么",而本插件精确追踪"这些提供的内容被如何使用"。通过这种双向标记,webpack 能够精确识别未使用的代码并将其移除。
该插件接收一个 global
参数决定是执行全局分析还是按运行时环境分别分析,影响最终的代码分割和优化策略。
2.1 初始化与准备阶段
constructor(global) {
this.global = global;
}
apply(compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
const moduleGraph = compilation.moduleGraph;
compilation.hooks.optimizeDependencies.tap(
{ name: PLUGIN_NAME, stage: STAGE_DEFAULT },
modules => {
// 缓存检查
if (compilation.moduleMemCaches) {
throw new Error("optimization.usedExports can't be used with cacheUnaffected...");
}
const logger = compilation.getLogger(PLUGIN_LOGGER_NAME);
const exportInfoToModuleMap = new Map();
const queue = new TupleQueue();
插件在 optimizeDependencies
钩子的默认阶段执行,确保在所有模块解析完成但优化尚未开始时运行。初始化阶段建立了两个关键数据结构:
-
exportInfoToModuleMap
: exportInfoToModuleMap
是一个映射表,将导出信息对象关联回其所属的模块。它解决了嵌套导出场景下的模块定位问题 - 当处理深层嵌套导出路径(如obj.nested.property
)时,需要找到嵌套导出信息所属的模块,以便在导出状态变化时将正确的模块加入处理队列。
如下图所:

这些都是我们前面讲过的。
-
queue
: 用于存储待处理的模块和其运行时环境信息
2.2 模块导出使用处理器 (processReferencedModule)
这个核心函数精确标记每个导出的使用状态:
const processReferencedModule = (module, usedExports, runtime, forceSideEffects) => {
const exportsInfo = moduleGraph.getExportsInfo(module);
if (usedExports.length > 0) {
// 处理模块有被使用的导出
这部分逻辑根据不同情况进行处理:
-
无导出类型模块处理:
if (!module.buildMeta || !module.buildMeta.exportsType) {
if (exportsInfo.setUsedWithoutInfo(runtime)) {
queue.enqueue(module, runtime);
}
return;
}
对于 CommonJS 或没有明确导出类型的模块,由于无法静态分析导出,标记为"完全使用"。
-
精确导出路径处理:
for (const usedExportInfo of usedExports) {
// 提取导出路径和混淆选项
let usedExport;
let canMangle = true;
if (Array.isArray(usedExportInfo)) {
usedExport = usedExportInfo;
} else {
usedExport = usedExportInfo.name;
canMangle = usedExportInfo.canMangle !== false;
}
这段代码处理两种导出引用格式:简单数组形式和带混淆标志的对象形式。如下代码所示:
import { moment } from "./moment";
moment.click();
最终输出结果如下:

-
整体导出对象使用:
if (usedExport.length === 0) {
if (exportsInfo.setUsedInUnknownWay(runtime)) {
queue.enqueue(module, runtime);
}
}
空数组表示使用了整个导出对象(如import * as mod from './mod'
),将导出标记为"未知方式使用"。
如下代码所示:
// moment.js
export const a = 1;
export const b = 2;
export const c = 3;
export const d = 4;
export const e = 5;
export const f = 6;
// index.js
import * as moment from "./moment";
console.log(moment);
最终输出结果如下图所示:

-
副作用处理:
} else {
// 没有使用导出,但可能有副作用
if (
!forceSideEffects &&
module.factoryMeta !== undefined &&
module.factoryMeta.sideEffectFree
) {
return; // 跳过无副作用的未使用模块
}
if (exportsInfo.setUsedForSideEffectsOnly(runtime)) {
queue.enqueue(module, runtime);
}
}
当模块没有被使用的导出时,检查是否有副作用:
-
如果标记为无副作用且未强制保留副作用,则完全跳过
-
否则标记为"仅用于副作用"
2.3 小结
FlagDependencyUsagePlugin 是 webpack 树摇机制的核心实现,负责精确标记哪些导出被使用,哪些可以安全移除。在 Seal 阶段,它通过全面分析依赖图来优化最终产物体积。
主要流程如下:
-
初始化阶段:通过 compilation.hooks.optimizeDependencies
钩子触发插件执行,为每个模块创建导出信息 (exportInfo) 并存入 exportInfoToModuleMap,调用 setHasUseInfo()
初始化导出使用状态追踪。
-
入口分析:从所有入口依赖开始,调用 processEntryDependency
函数,处理全局入口、命名入口及其包含的依赖,初始入口模块默认标记为"副作用使用",确保入口代码保留。
-
依赖遍历与标记:使用队列和广度优先搜索遍历整个依赖图,对每个模块调用 processModule
收集其依赖引用的导出,通过 getDependencyReferencedExports
确定每个依赖使用了哪些导出,使用 setUsedConditionally
将被引用的导出标记为已使用。
-
特殊情况处理:对整个导出对象引用 (EXPORTS_OBJECT_REFERENCED) 特殊处理,对无导出引用但有副作用的模块标记为仅副作用使用,对无副作用 (sideEffectFree) 模块在无导出使用时可能完全跳过。
-
结果存储:所有标记结果记录在 exportInfo._usedInRuntime
属性中,这些信息将直接影响代码生成阶段保留或移除哪些代码。
这个精确的导出使用分析是 webpack 实现高效树摇的关键环节,确保最终包中只包含实际使用的代码,显著优化应用性能和加载时间。无论是基础类型导出还是复杂对象,插件都能准确追踪其使用情况,为后续优化提供可靠依据。
3. 生成代码
4. 删除 Dead Code
在执行了 FlagDependencyExportsPlugin 和 FlagDependencyUsagePlugin 插件的导出标记和使用标记过程后,Webpack 将会准确地知道哪些模块和导出被实际使用,哪些是未使用的。接下来,Webpack 会进行 删除未使用代码(Dead Code Elimination)的优化,这个过程在 Tree Shaking 的最后阶段,确保未被使用的代码从最终的打包结果中移除。
首先我们需要编写这样的 webpack 配置:
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
mode: "production",
optimization: {
usedExports: true,
sideEffects: true,
minimize: true,
minimizer: [
new TerserPlugin({
extractComments: false,
}),
],
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
};
最终我们要编写这样的实际代码:

在这里,我们在 moment.js
文件中定义了三个变量,导出了两个,有一个没有导出,我们再 index.js 文件中只使用了一个,当我们执行构建的时候,最终输出的结果如下图所示:
(() => {
"use strict";
console.log(1);
})();
完美 🎉🎉🎉🎉🎉🎉
总结
Tree Shaking 是一种基于 ES Module 的静态代码分析技术,通过精确识别并移除未被引用的代码来减小最终打包体积。它通过"标记-清除"两阶段流程实现:先由 FlagDependencyExportsPlugin 识别模块提供了什么,再由 FlagDependencyUsagePlugin 标记这些导出如何被使用,最后在生产环境中通过压缩工具物理移除未使用代码。Tree Shaking 的成功依赖于 ES Module 的静态结构特性,这也是它无法在 CommonJS 等动态模块系统中有效工作的根本原因。