源码分析(九):优化 chunk
chunk 的一些优化
在 chunk
生成后,开始进行 chunk
优化之类的处理。
在触发钩子 optimize,optimizeModules
(module
相关的优化)等之后,忽略掉本次打包未触发插件的钩子,执行:
this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups);
触发插件:
EnsureChunkConditionsPlugin
处理chunkCondition
RemoveEmptyChunksPlugin
移除空chunk
MergeDuplicateChunksPlugin
处理重复chunk
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups);
触发插件:
SplitChunksPlugin
优化切割chunk
RemoveEmptyChunksPlugin
再次移除空chunk
设置 module.id
this.hooks.reviveModules.call(this.modules, this.records);
触发插件 RecordIdsPlugin
:设置 module.id
this.hooks.beforeModuleIds.call(this.modules);
触发插件 NamedModulesPlugin
: 设置 module.id
为 文件相对路径,然后执行:
this.applyModuleIds();
这一步主要用于设置 module.id
(如果 id
在上一步没有设置的话),内部具体算法为:
先遍历各 module
,找出其中最大的 id
以他为最大值(usedIdmax
),计算出比他小的所有未使用的正整数和(usedIdmax+1
)作为 unusedIds
用于给没有设置 id
的 module
使用,unusedIds
用尽后,则设置 id
为 (usedIdmax+1)++
this.sortItemsWithModuleIds();
根据 module.id
给 module,chunk,reasons
等排序。
设置 chunk.id
this.hooks.reviveChunks.call(this.chunks, this.records);
触发插件 RecordIdsPlugin
:设置 chunk.id
this.hooks.optimizeChunkOrder.call(this.chunks);
触发插件 OccurrenceOrderChunkIdsPlugin
:chunks
排序
this.hooks.beforeChunkIds.call(this.chunks);
触发插件 NamedChunksPlugin
:设置 chunk.id = chunk.name
this.applyChunkIds();
这一步主要用于设置 chunk.id
,算法与 this.applyModuleIds
一致。
this.sortItemsWithChunkIds();
根据 chunk.id
给 module,chunk,reasons,errors,warnings,children
等排序。
if (shouldRecord) {
this.hooks.recordModules.call(this.modules, this.records);
this.hooks.recordChunks.call(this.chunks, this.records);
}
依旧是对 records
的一些设置。
创建 hash
接下来执行:
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
if (shouldRecord) {
this.hooks.recordHash.call(this.records);
}
进入 createHash
,前文已介绍生成 hash
的方法,此处先初始化一个 hash
,然后执行:
this.mainTemplate.updateHash(hash);
this.chunkTemplate.updateHash(hash);
mainTemplate
: 渲染生成包含webpack runtime bootstrap
代码的chunk
chunkTemplate
: 渲染生成普通chunk
mainTemplate
在 update('maintemplate','3')
后,触发 MainTemplate.hooks
: hash
,执行插件 JsonpMainTemplatePlugin
,WasmMainTemplatePlugin
内的相关事件,hash.buffer
更新为 maintemplate3jsonp6WasmMainTemplatePlugin2
。
chunkTemplate
在 update('ChunkTemplate','2')
后,触发ChunkTemplate.hooks
: hash
,执行插件 JsonpChunkTemplatePlugin
内的相关事件,hash.buffer
更新为 maintemplate3jsonp6WasmMainTemplatePlugin2ChunkTemplate2JsonpChunkTemplatePlugin4webpackJsonpwindow
。
for (const key of Object.keys(this.moduleTemplates).sort()) {
this.moduleTemplates[key].updateHash(hash);
}
// 以下代码为 complation 实例化的时候所定义
this.moduleTemplates = {
javascript: new ModuleTemplate(this.runtimeTemplate, 'javascript'),
webassembly: new ModuleTemplate(this.runtimeTemplate, 'webassembly'),
};
将 this.moduleTemplates
的 key
排序后执行各自的 updateHash
,hash.buffer
更新为 maintemplate3jsonp6WasmMainTemplatePlugin2ChunkTemplate2JsonpChunkTemplatePlugin4webpackJsonpwindow1FunctionModuleTemplatePlugin21
然后如果有 children,warnings,errors
也把他们的 hash
或者 message
update
进去。然后执行:
创建 module hash
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = /** @type {string} */ (moduleHash.digest(hashDigest));
module.renderedHash = module.hash.substr(0, hashDigestLength);
}
这里循环初始化了每个 module
的 hash
,并调用了每个 module
的 updateHash
:
module.updateHash(moduleHash);
//上面 module.updateHash 调用
hash.update(this._buildHash); //这里加入了 _buildHash
super.updateHash(hash);
//上面 super 调用
hash.update(`${this.id}`);
hash.update(JSON.stringify(this.usedExports));
super.updateHash(hash);
//上面 super 调用
//调用各自 dependencies,blocks,variables的 updateHash
for (const dep of this.dependencies) dep.updateHash(hash);
for (const block of this.blocks) block.updateHash(hash);
for (const variable of this.variables) variable.updateHash(hash);
最终得到 moduleHash.buffer
形如:ac01f98d10f099796d2f3d600c2592d1./src/a.jsnull0,28./src/b.jsnamespace./src/b.js29,57./src/c.jsnamespace./src/c.js58,121../github/test-loader/loader.js?number=20000!./src/e.jsnamespace../github/test-loader/loader.js?number=20000!./src/e.js./src/b.jsnamespace./src/b.jsaddaddnamespacenullnull./src/c.jsnamespace./src/c.jssubsubnamespacenullnull../github/test-loader/loader.js?number=20000!./src/e.jsnamespace../github/test-loader/loader.js?number=20000!./src/e.jsdivdivnamespacenullnull
然后最终生成出 module
各自的 hash
和 renderedHash
。
创建 chunk hash
继续执行,先对 chunks
进行排序,然后遍历 chunks
:
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkHash = createHash(hashFunction);
try {
if (outputOptions.hashSalt) {
chunkHash.update(outputOptions.hashSalt);
}
chunk.updateHash(chunkHash);
const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;
template.updateHashForChunk(chunkHash, chunk, this.moduleTemplates.javascript, this.dependencyTemplates);
this.hooks.chunkHash.call(chunk, chunkHash);
chunk.hash = /** @type {string} */ (chunkHash.digest(hashDigest));
hash.update(chunk.hash);
chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
this.hooks.contentHash.call(chunk);
} catch (err) {
this.errors.push(new ChunkRenderError(chunk, '', err));
}
}
这里循环初始化了每个 chunk
的 hash
,并调用了每个 chunk
的 updateHash
:
chunk.updateHash(chunkHash);
//上面 chunk.updateHash 调用
hash.update(`${this.id} `);
hash.update(this.ids ? this.ids.join(',') : '');
hash.update(`${this.name || ''} `);
for (const m of this._modules) {
hash.update(m.hash); //此处把每个 module 的 hash 一并加入
}
得到 chunkHash.buffer
形如 bundle bundlebundle 99d78a1615d2e348fbf274adb4e0b67c4fa9f69c98e5b41607cb6354e95983c3824bbf3e0b5e82705f88a41a6741b08f2f18bdc137e8a1e8e6cc78ca7ce0caf64b500a96034ab069ecf31c34f944ede6
,然后判断 chunk
是否含有 runtime
代码( template
判断入口 chunk
与运行时 chunk
一致则为 this.mainTemplate
,不一致则为 this.chunkTemplate
)。
chunkTemplate
如果是 chunkTemplate
的 updateHashForChunk
,则执行:
this.updateHash(hash); //与上文 this.chunkTemplate.updateHash(hash) 执行相同
this.hooks.hashForChunk.call(hash, chunk);
this.hooks.hashForChunk.call(hash, chunk)
触发插件 JsonpChunkTemplatePlugin
相关事件, update
entryModule
和 group.childrenIterable
。
mainTemplate
如果是 mainTemplate
的 updateHashForChunk
,则执行:
this.updateHash(hash); //与上文 this.mainTemplate.updateHash(hash) 执行相同
this.hooks.hashForChunk.call(hash, chunk);
for (const line of this.renderBootstrap('0000', chunk, moduleTemplate, dependencyTemplates)) {
hash.update(line);
}
this.hooks.hashForChunk.call(hash, chunk)
触发插件 TemplatedPathPlugin
相关事件,根据 chunkFilename
的不同配置,update chunk.getChunkMaps
的不同导出,chunk.getChunkMaps
的实现为:
getChunkMaps(realHash) {
const chunkHashMap = Object.create(null);
const chunkContentHashMap = Object.create(null);
const chunkNameMap = Object.create(null);
for (const chunk of this.getAllAsyncChunks()) {
chunkHashMap[chunk.id] = realHash ? chunk.hash : chunk.renderedHash;
for (const key of Object.keys(chunk.contentHash)) {
if (!chunkContentHashMap[key]) {
chunkContentHashMap[key] = Object.create(null);
}
chunkContentHashMap[key][chunk.id] = chunk.contentHash[key];
}
if (chunk.name) {
chunkNameMap[chunk.id] = chunk.name;
}
}
return {
hash: chunkHashMap, // chunkFilename 配置为 chunkhash的导出
contentHash: chunkContentHashMap, // chunkFilename 配置为 contenthash 的导出
name: chunkNameMap // chunkFilename 配置为 name 的导出
};
}
可见各种类型的 hash
都与其他的 不含runtime模块即异步模块
的 hash
有强关联,所以前面的 chunk
排序也就很重要。
this.renderBootstrap
用于拼接 webpack runtime bootstrap
代码字符串。这里相当于把每一行 runtime
代码循环 update
进去,到此 chunk hash
生成结束。 将 chunk.hash
update
到 hash
上。 最终得到 chunk.hash
和 chunk.renderedHash
。
创建 content hash & fullhash & hash
然后执行:
this.hooks.contentHash.call(chunk);
这里触发 JavascriptModulesPlugin
相关事件,主要作用是创建生成 chunk.contentHash.javascript
,也就是 contentHash
生成相关,大体跟生成 chunk hash
一致.
最后在 createHash
里得到 Compilation.hash
和 Compilation.fullhash
,hash
生成到此结束。chunk
相关优化到此结束。
本章小结
- 本章主要是对
chunk
的一些优化工作,暴露了很多相关的优化钩子; - 设置了
module.id
及chunk.id
并排序; - 创建了
hash
,包括module hash,chunk hash,content hash,fullhash,hash