掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流
Hello,大家好,我是松寶寫代碼,寫寶寫的不止是代碼。接下來給大家?guī)淼氖顷P于Webpack4的性能優(yōu)化的系列,今天帶來的是編譯階段的性能優(yōu)化。

目前創(chuàng)新互聯(lián)公司已為千余家的企業(yè)提供了網(wǎng)站建設、域名、網(wǎng)絡空間、網(wǎng)站托管、企業(yè)網(wǎng)站設計、拱墅網(wǎng)站維護等服務,公司將堅持客戶導向、應用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
由于優(yōu)化都是在 Webpack 4 上做的,當時 Webpack 5 還未穩(wěn)定,現(xiàn)在使用 Webpack 5 時可能有些優(yōu)化方案不再需要或方案不一致,這里主要介紹優(yōu)化思路,僅作為參考。
在接觸一些大型項目構建速度慢的很離譜,有些項目在 編譯構建上30分鐘超時,有些構建到一半內(nèi)存溢出。但當時一些通用的 Webpack 構建優(yōu)化方案要么已經(jīng)接入,要么場景不適用:
在這種情況下,只好另辟蹊徑去尋找更多優(yōu)化方案,這篇文章主要就是介紹這些“非主流”的優(yōu)化方案,以及引發(fā)的思考。
簡化Webpack 的構建流程后,Webpack 的構建流程大體上分為如下幾個階段:
而在盡可能不改變處理邏輯的情況下,常見的優(yōu)化思路就是“并行”和“緩存”:
但目前“并行”和“緩存”僅覆蓋模塊編譯階段,能否把“并行”和“緩存”的方案擴展到整個構建流程呢?
為了讓“并行”+“緩存”能夠覆蓋整個構建流程,需要做如下準備工作:
引用透明改造包括如下幾個部分:
緩存池的核心功能:
并行調(diào)度池類似于數(shù)據(jù)庫連接池,主要功能:
編譯任務:使用 loader-runner 編譯模塊代碼。
壓縮任務:使用 terser/esbuild 壓縮模塊代碼。
SourceMap 任務:生成序列化 SourceNode。
做好了這些準備工作后,就可以開始進行各個階段的“并行”+“緩存”改造。
Webpack 內(nèi)部的單個模塊構建流程大致如下所示:
loader 運行類似于 Express/Koa 的中間件機制,每一個 Loader 分為 pitch 和 normal 兩個階段,cache-loader 利用這一點,在 pitch 階段進行緩存檢測,如果檢測到緩存可用則直接返回。無緩存或緩存不可用則繼續(xù)運行后續(xù)流程,直到 normal 階段生成緩存寫入文件系統(tǒng)。
thread-loader也是同理,只不過把后續(xù)的 loader 以及相關參數(shù)交給了子進程,并在子進程中模擬了 Webpack 的 loader 運行機制。
但 cache-loader 無法解決 AST Parser + 遍歷生成依賴帶來的消耗,開源界有 hard-source-webpack-plugin 嘗試解決這個問題(但問題很多)。Webpack 團隊自己也意識到了這個問題, 因此在 Webpack 5 中增加的 Persistent caching 來優(yōu)化,但它的實現(xiàn)思路是將 Webpack 整個上下文都緩存下來,因此 Webpack 5 給幾乎每個對象都增加了序列化/反序列化的方法:
// [email protected]/lib/NormalModule.js L1068 ~ L1105
serialize(context) {
const { write } = context;
// deserialize
write(this._source);
write(this._sourceSizes);
write(this.error);
write(this._lastSuccessfulBuildMeta);
write(this._forceBuild);
super.serialize(context);
}
deserialize(context) {
const { read } = context;
this._source = read();
this._sourceSizes = read();
this.error = read();
this._lastSuccessfulBuildMeta = read();
this._forceBuild = read();
super.deserialize(context);
}但由于當時無法升級 Webpack 5,且 Persistent caching 脫離了統(tǒng)一的緩存控制,最終選擇自己實現(xiàn)緩存來保證可移植、可拼接、預生成,如果在 Webpack 5 上實現(xiàn),理論上可以復用一部分模塊、依賴的序列化/反序列化能力,并橋接到緩存池上。
方案如下圖所示:
模塊的序列化分為兩部分:模塊本體序列化、模塊依賴序列化。
模塊本體的序列化較為簡單:
模塊的依賴序列化較為復雜,因為依賴由 Webpack 解析 AST 后遍歷生成,依賴內(nèi)部會直接保留相關聯(lián)的 AST 節(jié)點,這些 AST 節(jié)點在后續(xù)的 chunk 產(chǎn)物生成的 dependency template 階段會用來生成模塊引用依賴的相關代碼。
但實際上,依賴內(nèi)部并不會真正使用多少 AST 的節(jié)點,僅僅是從其中讀取少量信息用來做代碼替換的位置判斷和字符串拼接,因此序列化的過程就變成了提取 AST 上依賴使用的關鍵信息,而反序列化則是將這些關鍵信息偽造成 AST 節(jié)點即可。
不過,Webpack 內(nèi)部這樣的依賴有數(shù)十個(webpack/lib/dependencies目錄下),需要一個個處理。同時,對于一些特殊的場景,比如 Block 類型的依賴(通常是異步加載的代碼)無法支持。(Webpack 5 中可以直接用這些 Dependency 上面的序列化/反序列化方法)。
'use strict';
const NullDependency = require('./NullDependency');
class HarmonyExportHeaderDependency extends NullDependency {
constructor(range, rangeStatement) {
super();
this.range = range;
this.rangeStatement = rangeStatement;
}
get type() {
return 'harmony export header';
}
}
HarmonyExportHeaderDependency.Template = class HarmonyExportDependencyTemplate {
apply(dep, source) {
const content = '';
const replaceUntil = dep.range ? dep.range[0] - 1 : dep.rangeStatement[1] - 1;
source.replace(dep.rangeStatement[0], replaceUntil, content);
}
};
module.exports = HarmonyExportHeaderDependency;如此這般,當緩存命中時,模塊的依賴解析流程會被完全跳過。但這個流程并行化難度較高,主要原因是 Webpack 內(nèi) Parser Hooks 的橋接較為復雜,可以說 Hooks 的存在本身就是副作用的一種體現(xiàn)。
對 Webpack 的 enhance-resolver 進行緩存,降低 Webpack 在文件系統(tǒng)中查找的成本。由于 Resolver 較為復雜,且不同的 node_modules 組織方式、不同的依賴版本、不同的起始路徑,都可能使得相同的 request 被解析到完全不同的文件,因此針對不同類型的 request,緩存的處理邏輯不同:
構建器和 Webpack 的處理流程中存在大量的 Hash 計算。而使用 md5 作為 Hash 的成本較高,可以采用如 imurmurhash 等碰撞率高一些但性能更好的 Hash 方案進行替換。同時代理的 Hash 也可用來做后續(xù)的可移植緩存。

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流