SplitChunksPlugin 原理
配置文档
默认cacheGroups
在webpack/lib/webpack.js文件中,createCompiler时调用applyWebpackOptionsDefaults函数。该函数会为cacheGroups设置两个默认值,这两个默认值对应webpack的两个默认分包原则。其中defaultVendors是应用于node_modules:
F(cacheGroups, "default", () => ({
idHint: "",
reuseExistingChunk: true,
minChunks: 2,
priority: -20
}));
F(cacheGroups, "defaultVendors", () => ({
idHint: "vendors",
reuseExistingChunk: true,
test: NODE_MODULES_REGEXP,
priority: -10
}));触发时机
在Compilation.js文件中,调用seal方法时,当调用buildChunkGraph之后,就建立起了modules和chunks的关系,此时会开始触发hooks.optimizeChunks钩子:
while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {
/* empty */
}hooks.optimizeChunks钩子触发以下几个插件:
RemoveEmptyChunksPlugin:
for (const chunk of chunks) {
// 移除不包含 runtime 的空 chunk
if (
chunkGraph.getNumberOfChunkModules(chunk) === 0 &&
!chunk.hasRuntime() &&
chunkGraph.getNumberOfEntryModules(chunk) === 0
) {
compilation.chunkGraph.disconnectChunk(chunk);
compilation.chunks.delete(chunk);
}
}MergeDuplicateChunksPlugin:
// 合并”重复“的chunk。
if (chunkGraph.canChunksBeIntegrated(chunk, otherChunk)) {
chunkGraph.integrateChunks(chunk, otherChunk);
compilation.chunks.delete(otherChunk);
}SplitChunksPlugin:进行分包。
SplitChunksPlugin
SplitChunksPlugin主要有以下几个作用:
- 提取公共代码:比如不同
entry中引入了相同的模块,此时可以提取出来。 - 拆分过大的
js文件:比如从主模块中将node_modules中的代码单独拎出来。 - 合并零散的
js文件。
这几个功能主要都是由cacheGroups实现,在初始化阶段,已经定义好了两个默认的cacheGroups。
在webpack/lib/optimize中找到SplitChunksPlugin插件:
compilation.hooks.optimizeChunks.tap(
{
name: "SplitChunksPlugin",
stage: STAGE_ADVANCED
},
chunks => {}
)该插件回调函数会在hooks.optimizeChunks钩子触发时执行:
// Compilation.js 文件中触发
while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {
/* empty */
}此时已经构建好了modules和chunks之间的关系,但是还没有为chunks生成具体的代码。
举例
假设有两个入口文件index1.js和index2.js,他们同时引入了moduleA.js文件,webpack的配置如下:
// webpack.config.js
{
splitChunks: {
chunks: 'all',
cacheGroups: {
common: {
minSize: 1,
priority: 20,
minChunks: 2,
}
}
}
}匹配cacheGroups
SplitChunksPlugin首先会遍历所有modules,然后根据定义好cacheGroups的规则进行匹配:
for (const module of compilation.modules) {
let cacheGroups = this.options.getCacheGroups(module, context);
if (!Array.isArray(cacheGroups) || cacheGroups.length === 0) {
continue;
}
for (const cacheGroupSource of cacheGroups) {
const cacheGroup = this._getCacheGroup(cacheGroupSource);
// ...
}
}并且遍历所有的cacheGroups,进行下一步处理:
// 1. 获取 module 关联的 chunks
// 对于只有一个 chunk 使用该 module 时,通常只返回 [chunk]
// 对于多个chunk 使用该 module 时,通常返回 [new Set(chunk1, chunk2), chunk1, chunk2]
const combs = cacheGroup.usedExports
? getCombsByUsedExports()
: getCombs();
for (const chunkCombination of combs) {
// 如果是 Chunk,说明只有一个 chunk
const count =
chunkCombination instanceof Chunk ? 1 : chunkCombination.size;
// 2. 如果chunk的使用数小于minChunks,那么不符合要求,直接退出
if (count < cacheGroup.minChunks) continue;
const { chunks: selectedChunks, key: selectedChunksKey } =
getSelectedChunks(chunkCombination, cacheGroup.chunksFilter);
// 3. 将结果记录到chunksInfoMap中
addModuleToChunksInfoMap(
cacheGroup,
cacheGroupIndex,
selectedChunks,
selectedChunksKey,
module
);
}chunksInfoMap结构如下
chunksInfoMap.set(
// 使用到的 chunks 形成的 key
key,
(info = {
// 同一 cacheGroup 匹配到的 module 且在同样的 chunks 中使用时
// 将这些 module 存于此处
modules: new SortableSet(
undefined,
compareModulesByIdentifier
),
cacheGroup,
cacheGroupIndex,
name,
// 对象形式,记录各种不同的资源的大小,比如:{ javascript: 200 }
sizes: {},
// 包含的 chunks
chunks: new Set(),
reuseableChunks: new Set(),
chunksKeys: new Set()
})
);这样所有modules经过与cacheGroup匹配后,形成的chunksInfoMap就能表示每个cacheGroup涉及到了哪些module,而这些module又在哪些chunks里被用到。
过滤chunksInfoMap
得到chunksInfoMap后,会先对其进行过滤:
for (const [key, info] of chunksInfoMap) {
if (removeMinSizeViolatingModules(info)) {
chunksInfoMap.delete(key);
} else if (
!checkMinSizeReduction(
info.sizes,
info.cacheGroup.minSizeReduction,
info.chunks.size
)
) {
chunksInfoMap.delete(key);
}
}chunksInfoMap实际上对应于待分包的chunk。通过removeMinSizeViolatingModules方法将chunks.size和minSize对比,如果小于minSize,那么将不符合分包的规定,因此将其剔除掉。
生成新chunk
遍历chunksInfoMap,生成新的chunk:
while (chunksInfoMap.size > 0) {
let bestEntryKey;
let bestEntry;
for (const pair of chunksInfoMap) {
const key = pair[0];
const info = pair[1];
if (
bestEntry === undefined ||
compareEntries(bestEntry, info) < 0
) {
bestEntry = info;
bestEntryKey = key;
}
}
const item = bestEntry;
chunksInfoMap.delete(bestEntryKey);
// ...
}首先会通过compareEntries方法对比优先级,看哪个cacheGroup对应的chunk优先生成。经过一系列的处理后,最后会生成一个空chunk:
if (newChunk === undefined) {
newChunk = compilation.addChunk(chunkName);
}然后对于cacheGroups的module涉及到的其他chunks(也就是usedChunks),调用split方法进行分包:
for (const chunk of usedChunks) {
chunk.split(newChunk);
}
// Chunk 的 split 方法
split(newChunk) {
// 对于每个使用到 chunk 的地方,newChunk 也应该被使用
for (const chunkGroup of this._groups) {
chunkGroup.insertChunk(newChunk, this);
newChunk.addGroup(chunkGroup);
}
for (const idHint of this.idNameHints) {
newChunk.idNameHints.add(idHint);
}
newChunk.runtime = mergeRuntime(newChunk.runtime, this.runtime);
}对newChunk进行处理,其中比较重要的是modules和usedChunks的处理:
// usedChunks 里面需要移除所有已经分包出去的 modules
for (const module of item.modules) {
for (const chunk of usedChunks) {
chunkGraph.disconnectChunkAndModule(chunk, module);
}
}
for (const [key, info] of chunksInfoMap) {
// 如果与后续处理的 chunks 存在相同 chunk
if (isOverlap(info.chunks, usedChunks)) {
let updated = false;
for (const module of item.modules) {
// 如果后续处理的 modules 包含此 module,那么需要删除掉,后续不再对该module分包
if (info.modules.has(module)) {
info.modules.delete(module);
for (const key of module.getSourceTypes()) {
info.sizes[key] -= module.size(key);
}
updated = true;
}
}
if (updated) {
if (info.modules.size === 0) {
chunksInfoMap.delete(key);
continue;
}
if (
removeMinSizeViolatingModules(info) ||
!checkMinSizeReduction(
info.sizes,
info.cacheGroup.minSizeReduction,
info.chunks.size
)
) {
chunksInfoMap.delete(key);
continue;
}
}
}
}至此,对于每个cacheGroup就已经生成了相应的chunk。
处理size
得到chunks后还会进一步对chunk处理,如chunk过大时,会再次进行分包。
// 将 chunk 再次细分
const results = deterministicGroupingForModules({
// ...
})总结
SplitChunksPlugin在hooks.optimizeChunks钩子触发时执行,此时modules和chunks的关系已建立,但还未进行code generate。
SplitChunksPlugin主要用于提取公共代码,拆分或合并代码等,其核心原理如下:
- 通过
cacheGroups匹配modules,生成chunksInfoMap。确定每个cacheGroups对应哪些modules,以及这些modules所在的chunks。 - 遍历
chunksInfoMap,根据cacheGroups里的modules生成新chunk。断开这些modules和原有的chunks的关系,将新chunk加入到原有chunks的chunkGroups当中。 - 对分包后的
chunks再次进行处理,如果体积过大就会进行再次分包。
