广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Webpack 5 新特性 Concluding Universe 结束宇宙是什么?

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

Webpack 5 新特性 Concluding Universe 结束宇宙是什么?

为什么叫结束宇宙?因为 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 把构建收尾工作工程化的一次尝试。它把原先散落在不同阶段的冻结、合并和哈希计算集中起来,让插件开发者有一个明确的观察点,也让持久化缓存和模块联邦有了可靠的最终状态保证。理解了它的触发时机和配置影响,就能更有效地优化大型项目构建流程。

广告位 · 文章底部(后台-模块管理替换此区域)