Webpack 5 对模块排序逻辑的改造,最容易被忽略却又直接影响产物稳定性的部分,就是内部被称为 Sort Universe 的种类宇宙体系。简单来说,它不再像 Webpack 4 那样把所有模块放在一个平铺列表里统一排序,而是先根据模块的资源类型和运行时角色划分到不同的排序宇宙中。每个宇宙拥有独立的比较器与优先级,编译器在生成 chunk 时先处理高优先级宇宙,再处理低优先级宇宙。这样的设计让模块初始化顺序更加可控,也使得入口模块、异步模块、共享依赖和运行时模块之间的先后关系更加清晰。下面这张图展示了不同种类宇宙在 chunk 组装过程中的位置关系。

什么是 Sort Universe 与种类宇宙的层级划分
Sort Universe 并不是一个暴露给用户的独立配置项,而是 Webpack 5 源码中用于描述排序规则集合的一个抽象概念。在 Compilation.js 的模块排序阶段,编译器会调用内部的 sortModules 方法,该方法首先读取一个全局的宇宙优先级表。这个表把模块分成若干类型,每一种类型对应一个排序宇宙。常见的宇宙包括普通 JavaScript 模块、Context 模块、运行时模块、Concatenated 模块,以及通过 import() 动态引入的异步模块。不同宇宙之间不会直接混合比较,而是先按宇宙优先级确定先后顺序。
这种层级划分的价值在于解决了 Webpack 4 中一个历史遗留问题:当 chunk 中同时包含入口模块、异步依赖和运行时辅助函数时,简单的 identifier 排序会导致运行时模块可能夹在普通业务模块中间。一旦运行时模块插入位置变化,产物中的模块执行顺序就可能改变,进而影响哈希值。Webpack 5 通过优先放置运行时宇宙,再放置同步依赖宇宙,最后处理异步宇宙,构建结果的可预测性得到明显提升。对于用户来说,即使不改任何配置,升级到 Webpack 5 后也会发现多页面应用的公共 chunk 内容更加稳定。
种类宇宙的实现并非直接硬编码在某一处,而是通过比较器工厂函数动态生成。源码中常见的 compareModulesByIdentifier 只是最底层的兜底比较器,外层还会包裹 compareModulesByPreOrderIndexOrIdentifier、compareModulesByPostOrderIndexOrIdentifier 等组合比较器。这些比较器共同构成一个排序宇宙的规则集合。开发者在阅读源码时如果不了解这种分层思想,很容易误以为模块排序只是简单的字符串比较,从而错过关键的优先级逻辑。
Sort Universe 如何影响模块排序与产物哈希
Sort Universe 对产物哈希的影响主要体现在两个层面。第一个层面是模块排序结果直接决定 chunk 内模块 ID 的分布。如果使用 optimization.moduleIds = 'deterministic',模块 ID 会根据模块的相对路径和排序位置生成。排序越稳定,ID 越稳定。第二个层面是 chunk 文件的最终内容顺序会参考排序宇宙的优先级,不同构建之间只要依赖关系不变,文件中的模块包装函数顺序就不会发生漂移。这种稳定性是长期缓存策略能够生效的基础。
下面给出一个典型的 Webpack 5 配置,它结合了确定性模块 ID 和合理的代码分割策略,能让 Sort Universe 的排序优势最大化。
// webpack.config.js
module.exports = {
mode: 'production',
entry: {
main: './src/index.js',
admin: './src/admin.js',
},
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
clean: true,
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true,
},
},
},
},
};
上述配置中,runtimeChunk: 'single' 会把 Webpack 的运行时辅助代码单独抽出,形成最高优先级的运行时宇宙。moduleIds: 'deterministic' 则保证模块 ID 不依赖构建机器的文件枚举顺序。两者配合之后,Sort Universe 在每次构建时都会按照固定的宇宙优先级和组内比较器工作。即使多台开发机并行构建,产物的内容哈希也能保持一致。这一点在团队协作和 CI 缓存场景下尤为重要。
需要注意的是,Sort Universe 不是万能药。它只能保证排序规则本身的确定性,如果依赖关系发生变化,模块在宇宙中的位置依然会改变。例如一个原本属于异步宇宙的模块被改成同步引入,它的优先级会从低优先级调整到高优先级,chunk 内容自然也会变化。这种变化是符合预期的,开发者不应该为了追求哈希不变而强行固化排序结果。真正需要关注的是排序规则是否在所有环境下执行一致。
常见误区与自定义插件带来的排序干扰
在 Webpack 5 的实际使用中,有一类问题经常被误认为是 Sort Universe 的缺陷,那就是自定义插件扰乱了默认排序。Webpack 的插件系统允许在 compilation.hooks.optimizeModules 阶段修改模块顺序,但如果插件没有正确处理模块宇宙的优先级,就可能把低优先级的异步模块提前到运行时模块之前。这种情况下构建虽然不会报错,但浏览器执行时会因为运行时函数尚未初始化而出现 Cannot read property 'call' of undefined 之类的错误。
以下是一段有问题的插件代码示例,它试图按照文件大小对模块重新排序,却忽略了宇宙优先级。
// 自定义插件错误示例:破坏 Sort Universe 优先级
class DangerousSortPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('DangerousSortPlugin', (compilation) => {
compilation.hooks.optimizeModules.tap('DangerousSortPlugin', (modules) => {
modules.sort((a, b) => {
return a.size() - b.size();
});
});
});
}
}
module.exports = {
plugins: [new DangerousSortPlugin()],
};
这段代码直接对模块数组调用 sort,完全绕过了 Webpack 内部的宇宙比较器。如果恰好一个体积很小的运行时模块被排到后面,它的初始化顺序就会晚于依赖它的普通模块,最终导致运行时报错。更隐蔽的情况是,模块排序变化看似没有立即引发错误,但产物哈希在不同构建之间来回跳动。要避免这类问题,应该使用 compilation.modules 的默认排序方法,或者在自己的比较器里优先比较模块的 getNumberOfFromModules 之类的依赖关系,而不是直接操作数组顺序。
另一个常见误区是把 optimization.usedExports 或 sideEffects 与排序稳定性混为一谈。这些配置影响的是模块内部代码的裁剪范围,而不是模块之间的顺序。Sort Universe 只负责决定模块在 chunk 中的排列位置,不会改变模块本身是否被保留。因此,如果开启 sideEffects 后哈希变化,应该从依赖关系变化的角度排查,而不是怀疑排序宇宙出了问题。
从源码视角理解排序宇宙的稳定性保障
Webpack 5 的排序稳定性依赖于两个核心机制。第一是使用稳定的比较器,例如 compareStrings 和 compareNumbers 都不会因为对象遍历顺序变化而产生不同结果。第二是宇宙优先级表在构建过程中保持不变,不会受到模块数组初始顺序的影响。这种设计参考了稳定的拓扑排序思想,即在存在依赖关系时优先按依赖边排序,没有依赖关系时则回退到标识符排序。
下面展示一个简化版的排序宇宙比较器实现,它模拟了 Webpack 5 内部先按宇宙优先级分组,再按标识符排序的逻辑。
// 简化版 Sort Universe 比较器
const universePriority = {
runtime: 100,
normal: 80,
context: 60,
async: 40,
};
function createModuleComparator(modules) {
const moduleMap = new Map();
modules.forEach((module, index) => {
moduleMap.set(module, index);
});
return function compareModules(a, b) {
const typeA = getModuleUniverse(a);
const typeB = getModuleUniverse(b);
const priorityDiff = universePriority[typeA] - universePriority[typeB];
if (priorityDiff !== 0) {
return -priorityDiff; // 高优先级排在前面
}
const idA = a.identifier();
const idB = b.identifier();
if (idA < idB) return -1;
if (idA > idB) return 1;
return 0;
};
}
function getModuleUniverse(module) {
if (module.isRuntimeModule) return 'runtime';
if (module.type === 'context') return 'context';
if (module.isAsync) return 'async';
return 'normal';
}
在实际源码中,比较器会更复杂,需要处理 Concatenated 模块、External 模块和 Delegated 模块等特殊情况。但核心思想没有变化:先确定宇宙类别,再在类别内部进行稳定比较。这样即使模块数组的初始顺序因为文件系统遍历不同而发生变化,最终排序结果也保持一致。配合 deterministic 模块 ID 策略,Webpack 5 可以在多平台、多构建环境下实现真正的可重复构建。
理解 Sort Universe 之后,开发者就能更有信心地调整 splitChunks 配置。例如把共享依赖拆成单独的 vendor chunk,实际上就是把这些模块归入一个独立的缓存组宇宙。它们在排序时会被赋予对应的优先级,从而稳定地出现在业务代码之前。这种设计让代码分割不再只是体积优化,更成为提升运行时可预测性的重要手段。面对排序相关的构建问题,先从宇宙优先级和组内比较器两个维度排查,往往能快速定位根因。
Webpack 5Sort Universe模块排序修改时间:2026-09-22 22:19:44