Webpack 5 在构建流程即将完成时会进入一个独立的收尾阶段,官方内部把它称为 Concluding Universe,直译过来就是结束宇宙。这个阶段不会新增任何业务模块,也不会改动依赖图的结构,它的核心工作是冻结模块宇宙中的最终状态,生成 chunk 元数据,并把运行时不同版本之间的差异统一起来。与 webpack 4 把大量收尾逻辑直接堆在 seal 阶段的做法不同,webpack 5 将这部分工作拆开后,增量构建的稳定性明显提升。下面先梳理这个阶段出现的背景和它要解决的痛点。

为什么叫结束宇宙?因为 webpack 把每一个入口依赖图看作一个独立宇宙,当所有模块都完成连接后,这个宇宙需要被终结。结束宇宙阶段会遍历整个 chunk graph,确认每个 chunk 的父子关系、入口点和运行时引用都是最新的,随后生成一份只读的 moduleGraphHash。这份哈希值会作为后续构建的比对基准,如果下一次构建时模块内容没有变化,结束宇宙阶段可以直接复用上一轮的冻结结果,省掉大量遍历开销。
一、Concluding Universe 的触发时机与内部执行顺序
Concluding Universe 并不是一个可以随意调用的公开 API,它由 compiler 在每次 compilation 的 seal 阶段完成后自动触发。具体顺序是:先完成模块解析和依赖收集,再执行所有模块的 seal 操作,然后进入 concluding universe。这个阶段一旦启动就不能被异步钩子打断,所有插件都需要通过同步钩子来观察或记录状态。
开发者可以在自定插件中监听 compilation 对象上的 concludingUniverse 钩子,查看 chunkGraph 是否已经被冻结。下面是一个简单示例:
class ConcludingUniversePlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap('ConcludingUniversePlugin', (compilation) => {
compilation.hooks.concludingUniverse.tap('ConcludingUniversePlugin', (chunkGraph) => {
console.log('concluding universe started');
console.log('chunk count:', chunkGraph.length);
});
});
}
}
module.exports = ConcludingUniversePlugin;
这段插件代码会输出结束宇宙阶段启动时的 chunk 数量。需要注意的是,这里的 chunkGraph 已经是接近最终形态的数据结构,不要再尝试修改它,否则会导致输出产物不一致。如果只做日志记录或性能监控,用这个钩子非常合适。
从内部执行顺序看,concluding universe 会先执行 moduleGraphHash 计算,再进行 runtime chunk 合并,最后生成 asset 列表。这个顺序是固定的,因为哈希计算依赖模块内容,运行时合并依赖哈希结果。了解这一点可以避免在插件中过早读取尚未生成的运行时信息。
二、通过配置调整 Concluding Universe 的收尾策略
虽然这个阶段由 webpack 内部自动调度,但 webpack 5 仍然在 experiments 和 optimization 中保留了一些调节空间。默认情况下,experiments.concludingUniverse 为开启状态,optimization 下也有对应的可选配置,用于控制是否冻结模块图、是否合并运行时 chunk,以及是否生成 moduleGraphHash。
module.exports = {
mode: 'production',
experiments: {
concludingUniverse: true
},
optimization: {
concludingUniverse: {
freezeModuleGraph: true,
mergeRuntimeChunks: true,
moduleGraphHash: true
}
}
};
如果把 freezeModuleGraph 设为 false,webpack 在结束宇宙阶段就不会冻结依赖图,后续插件仍然可以修改模块关系。但这种做法会破坏增量构建的缓存一致性,通常只在调试构建结果差异时临时使用。正式项目里不建议关闭这个选项。
mergeRuntimeChunks 的作用是把所有入口的运行时片段尽量合并到一个文件中,减少浏览器端重复加载运行时代码的次数。在多入口应用中,这项优化可以明显减少首屏请求数量。moduleGraphHash 则用于给当前的依赖图生成唯一标识,持久化缓存会用它判断上次构建结果是否仍然有效。三个选项默认都是开启的,除非你清楚关闭后的副作用,否则保持默认即可。
三、Concluding Universe 与持久化缓存、模块联邦的协作
持久化缓存是 webpack 5 最受关注的改进之一,而 concluding universe 在其中扮演了收尾确认的角色。每次构建结束时,结束宇宙阶段生成的 moduleGraphHash 会被写入缓存目录。下次构建开始时,webpack 会先比较模块文件的内容哈希和依赖图哈希,如果都匹配,就直接跳过解析和链接阶段,复用上轮的 chunk 结构。这个过程极大降低了大型项目的增量构建时间。
模块联邦同样依赖这个收尾阶段。远程模块的 remoteEntry 文件并不是在 seal 阶段就完全生成的,它需要等到 concluding universe 冻结模块图之后,才能确定哪些模块会被暴露给外部应用。下面是一个典型的模块联邦配置:
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js'
},
shared: {
react: { singleton: true, eager: true }
}
})
]
};
宿主应用在运行时会加载 app1 暴露的模块映射,而这个映射是在结束宇宙阶段被固定下来的。如果远程模块的运行时版本与宿主不兼容,构建过程会在 concluding universe 阶段直接报错,而不是等到浏览器运行时才暴露问题。因此,在微前端项目中观察这个阶段的输出日志,往往能更快定位到模块共享冲突。
总的来说,concluding universe 是 webpack 5 把构建收尾工作工程化的一次尝试。它把原先散落在不同阶段的冻结、合并和哈希计算集中起来,让插件开发者有一个明确的观察点,也让持久化缓存和模块联邦有了可靠的最终状态保证。理解了它的触发时机和配置影响,就能更有效地优化大型项目构建流程。

