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
再次进行处理,如果体积过大就会进行再次分包。