在 Webpack 5 的构建体系里,模块之间的引用关系会被抽象成一张有向图。当这张图中出现环时,就形成了循环依赖。传统的 Webpack 4 处理循环依赖时,往往会把相互引用的模块反复合并到多个 chunk 中,导致产物膨胀。Webpack 5 引入的 Cycle Universe 周期宇宙,正是为了识别这些环结构,并基于环来优化模块分发。虽然它并未作为一个显式的配置项暴露给开发者,但其算法已经渗透到 splitChunks、持久化缓存和模块联邦等多个子系统中。

循环依赖在大型前端项目中并不罕见。以状态管理为例,store 模块需要调用工具函数做序列化,而工具函数又反向引用了 store 读取默认值;再比如 React 组件与自定义 Hook 之间互相引用,都会形成有向图中的环。Webpack 如果不做额外处理,只能按照模块解析顺序强行切断某个边,这会带来两个后果:一是被切断的模块可能被复制到不同的 chunk,增加总体积;二是运行时执行顺序可能不符合开发者预期,出现 undefined 变量。Cycle Universe 通过把这个环识别为一个整体来化解这个问题。
一、循环依赖为什么会让构建产物膨胀
Webpack 在编译阶段会把每个文件解析为一个模块对象,并通过 import 或 require 语句建立模块之间的依赖关系。正常情况下,依赖图是一张有向无环图,Compiler 可以按照拓扑排序依次处理模块。但一旦两个模块相互引用,图中就出现了环。比如下面这个典型场景:
// a.js
import { bValue } from './b.js';
export const aValue = 'a' + bValue;
// b.js
import { aValue } from './a.js';
export const bValue = 'b' + aValue;
这段代码中 a 和 b 相互引用,Webpack 的模块解析器会先处理 a.js,发现它依赖 b.js,接着处理 b.js,又发现 b.js 依赖 a.js。为了防止无限递归,Webpack 会在模块缓存中标记 a.js 为处理中,当再次遇到 a.js 时直接返回一个空对象。这虽然避免了死循环,却导致 a.js 在执行时拿到的 bValue 可能是 undefined,进而产生运行时错误。对于更复杂的环,模块还可能被重复打包进不同的 chunk,比如业务代码 chunk 和 vendor chunk 都包含某些工具模块,最终增加了下载成本。
Webpack 4 面对这种情况,主要依赖 splitChunks 的合并策略来减少重复,但它并不感知环的存在,只能从体积和引用次数上做粗略判断。这样做的问题在于,环中模块的引用次数可能不高,但因为它们彼此依赖,只要其中一个被拉进新的 chunk,整个环都会被带过去。Webpack 5 的 Cycle Universe 就是针对这个盲区设计的,它把环作为最小处理单元,避免在拆包时错误地切断环内连接。
二、Cycle Universe 的建模与拆包逻辑
Cycle Universe 的核心思想来自图论中的强连通分量(Strongly Connected Component,简称 SCC)。一个强连通分量是指图中任意两个顶点之间存在双向可达路径的极大子图。在 Webpack 的模块依赖图中,一个循环依赖环就是一个典型的强连通分量。Webpack 5 在构建完成后,会先对模块图做一次 SCC 分解,把互相可达的模块划分到同一个周期宇宙中。同一个宇宙内的模块只会在宇宙内部互相引用,不会与其他宇宙形成新的环。
这个建模方式带来的好处非常直接:当 splitChunks 需要拆分 chunk 时,如果把某个宇宙中的模块单独抽走,就必须考虑整个宇宙的完整性。如果拆走一部分而留下另一部分,运行时可能仍然需要跨 chunk 动态加载,而且加载顺序一旦打乱,未初始化的导出值就会暴露。因此 Webpack 5 会倾向于把同一个周期宇宙的模块整体移动到同一个缓存组,只有在用户显式配置了更高优先级规则时,才会打破这个整体。
下面是一段与 Cycle Universe 配合使用的 splitChunks 配置。这里的 vendor-cycle 缓存组会把来自 node_modules 的循环依赖模块聚合到一个单独的 chunk 中,减少业务代码与第三方库之间的环所带来的重复:
module.exports = {
mode: 'production',
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
cycleUniverse: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor-cycle',
priority: -10,
reuseExistingChunk: true
}
}
}
}
};
上面的配置里,test 字段使用正则匹配 node_modules 路径,name 指定输出 chunk 名称,reuseExistingChunk 表示如果当前 chunk 已经包含这些模块,就复用而不是重复创建。借助周期宇宙的环识别,Webpack 5 在执行这个规则时,会把同一个强连通分量内的第三方模块全部归入 vendor-cycle chunk,而不是只抽取单个模块。
三、在项目中检测循环依赖并验证效果
要在实际项目里确认 Cycle Universe 是否生效,最直接的方式是分析 stats 输出。Webpack 5 的 stats 配置支持输出模块的依赖原因和所属 chunk,通过查看同一个模块是否出现在多个 chunk 中,可以判断环是否被正确合并。典型的配置如下:
module.exports = {
stats: {
modules: true,
reasons: true,
chunkModules: true,
chunks: true
}
};
执行构建后,终端会输出每个模块的完整信息。对于循环依赖模块,它的 reasons 数组会列出所有引用它的模块,而 dependencies 数组则显示它引用了谁。如果在 stats 中看到两个模块互相出现在对方的 reasons 里,就说明存在一个环。但手工排查大项目的 stats 非常低效,可以编写一个自定义插件来自动检测。
下面是一个轻量的 Cycle Universe 检测插件,它在 Webpack 完成模块解析后遍历所有模块,通过深度优先搜索发现环并输出警告:
class CycleUniversePlugin {
apply(compiler) {
compiler.hooks.compilation.tap('CycleUniversePlugin', function(compilation) {
compilation.hooks.finishModules.tap('CycleUniversePlugin', function(modules) {
const visited = new Set();
const stack = new Set();
function visit(module) {
if (stack.has(module.identifier())) {
console.warn('Cycle detected in:', module.identifier());
return;
}
if (visited.has(module.identifier())) return;
visited.add(module.identifier());
stack.add(module.identifier());
module.dependencies.forEach(function(dep) {
if (dep.module) visit(dep.module);
});
stack.delete(module.identifier());
}
modules.forEach(visit);
});
});
}
}
module.exports = CycleUniversePlugin;
这个插件会在编译期间打印出存在循环依赖的模块标识。需要注意的是,并非所有循环依赖都会导致问题,有些环在运行时不会访问未初始化的绑定,所以检测结果更多是作为排查线索,而不是必须消除的错误。
四、与持久化缓存和模块联邦的协同
Webpack 5 的持久化缓存会把模块图、chunk 关系以及编译结果序列化到磁盘,二次构建时直接读取缓存,跳过重复的模块解析。Cycle Universe 的环识别结果同样会被纳入缓存。如果某个循环依赖环没有被正确合并,它的模块指纹会频繁变化,导致持久化缓存失效。反过来,借助周期宇宙的稳定环划分,Webpack 可以精确记录哪些环中模块发生了变化,只对有变化的宇宙重新计算,从而显著提升增量构建速度。
在模块联邦场景中,循环依赖的复杂度更高。两个独立构建的远程应用可能互相引用对方暴露的模块,形成一个跨构建的依赖环。Webpack 5 的模块联邦通过 shared 配置将公共依赖提升为共享模块,而 Cycle Universe 则在共享模块的依赖图里继续识别环,避免远程模块被重复实例化。比如下面的配置将 react 和 react-dom 作为共享模块,配合自动拆分,可以防止远程应用因循环依赖加载两份运行时。
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app_shell',
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
当然,周期宇宙并不是万能药。对于运行时依赖初始化顺序的循环引用,即便 Webpack 正确划分了 chunk,执行阶段仍然可能拿到 undefined。遇到这类问题时,应当优先从代码层面解除环,例如将公共依赖下沉到独立模块,或者使用函数延迟引用。Cycle Universe 的价值主要体现在构建层,它能让拆包更合理、缓存更稳定,但无法修正应用本身的初始化顺序错误。
总体来看,Webpack 5 虽然没有把 Cycle Universe 做成一个单独的配置项,但它对循环依赖的建模思路已经融入了 splitChunks、持久化缓存和模块联邦的底层实现。理解这一机制,有助于我们在面对复杂依赖图时做出更合理的拆包决策,也能更好地解读 stats 输出,定位那些难以察觉的模块重复与运行时异常。
Webpack 5Cycle Universe循环依赖修改时间:2026-09-24 07:46:34