Webpack5 持久化緩存實(shí)踐
背景
公司的云his靜態(tài)項(xiàng)目代碼量巨大,依賴的npm包大概有100個(gè),打包一次大概要14分鐘
自研的hammer工具的本地打包雖然能提升部署時(shí)間,但是依賴開發(fā)的手動(dòng)操作
用來(lái)存放本地構(gòu)建產(chǎn)物的服務(wù)器容量滿了,所以為了正常使用本地打包功能,還得定期去清理服務(wù)器上的老文件,不夠方便
解決思路
node版本提升 8.x -> 12.x
利用webpack5的持久化緩存提升構(gòu)建效率
速度大幅度提升,快了7倍。
使用基于rust開發(fā)的swc替代babel,測(cè)試的構(gòu)建速度提升一分鐘半左右,因生態(tài)不成熟,不能上生產(chǎn)。
關(guān)鍵代碼
module.exports = {
...
cache: {
// 將緩存類型設(shè)置為文件系統(tǒng),默認(rèn)是memory
type: 'filesystem',
buildDependencies: {
// 更改配置文件時(shí),重新緩存
config: [__filename]
}
},
optimization: {
// 值為"single"會(huì)創(chuàng)建一個(gè)在所有生成chunk之間共享的運(yùn)行時(shí)文件
runtimeChunk: 'single',
moduleIds: 'deterministic',
},
}
webpack 在入口 chunk 中,包含了某些 boilerplate(引導(dǎo)模板),特別是 runtime 和 manifest。這些代碼如果不被單獨(dú)抽離會(huì)導(dǎo)致即使沒有代碼改動(dòng),打包出來(lái)的文件名仍然會(huì)改變,導(dǎo)致無(wú)法命中緩存。webpack4中使用HashedModuleIdsPlugin來(lái)生成hash值作為模塊id,在webpack5中已經(jīng)不需要了,moduleIds: 'deterministic',是用來(lái)保證模塊的id不會(huì)隨著解析順序的變化而變化,生產(chǎn)環(huán)境默認(rèn)開啟。
緩存的方式(從構(gòu)建層面來(lái)講)
webpack V4
- cache-loader:建議在開銷較大的loader前加,比如babel-loader、vue-loader等;
- dll:對(duì)不經(jīng)常改變版本的依賴(react、lodash),單獨(dú)生成動(dòng)態(tài)鏈接庫(kù)(bundle),提高構(gòu)建速度,需要DllPlugin 、DllReferencePlugin 搭配使用,通過(guò)引用 dll 的 manifest 文件來(lái)把依賴的名稱映射到模塊的 id 上,之后再在需要的時(shí)候通過(guò)內(nèi)置的 __webpack_require__ 函數(shù)來(lái) require 他們,推薦在開發(fā)模式下使用
webpack V5
- 文件系統(tǒng)緩存,配置方式見上面的關(guān)鍵代碼,作用是將Webpack運(yùn)行時(shí)存在于內(nèi)存中的那些緩存,不是loader的產(chǎn)物,更不是dll,根據(jù)Webpack運(yùn)行環(huán)境的不同,在dev開發(fā)時(shí)依舊使用MemoryCachePlugin,而在build時(shí)使用IdleFileCachePlugin。dev/build的二次編譯速度會(huì)遠(yuǎn)超cache-loader
一些原理淺談
Webpack 5令人期待的持久緩存優(yōu)化了整個(gè)構(gòu)建流程,原理依然還是那一套:當(dāng)檢測(cè)到某個(gè)文件變化時(shí),根據(jù)依賴關(guān)系,只對(duì)依賴樹上相關(guān)的文件進(jìn)行編譯,從而大幅提高了構(gòu)建速度。官方經(jīng)過(guò)測(cè)試,16000 個(gè)模塊組成的單頁(yè)應(yīng)用,速度竟然可以提高 98%!其中值得注意的是持久緩存會(huì)將緩存存儲(chǔ)到磁盤。
對(duì)于一個(gè)持續(xù)化構(gòu)建過(guò)程來(lái)說(shuō),第一次構(gòu)建是一次全量構(gòu)建,然后它會(huì)將相關(guān)產(chǎn)物序列化緩存在磁盤中(serialize)。后續(xù)構(gòu)建具體流程可以依賴于上一次的緩存進(jìn)行:讀取磁盤緩存 -> 校驗(yàn)?zāi)K -> 解封模塊內(nèi)容。因?yàn)槟K之間的關(guān)系并不會(huì)被顯式緩存,因此模塊之間的關(guān)系仍然需要在每次構(gòu)建過(guò)程中被校驗(yàn),這個(gè)校驗(yàn)過(guò)程和正常的 webpack 進(jìn)行分析依賴關(guān)系時(shí)的邏輯是完全一致的。
對(duì)于 resolver 的緩存同樣可以持久化緩存起來(lái),一旦 resolver 緩存經(jīng)過(guò)校驗(yàn)后發(fā)現(xiàn)準(zhǔn)確匹配,就可以用于快速尋找依賴關(guān)系。如果 resolver 緩存校驗(yàn)失敗的情況,將會(huì)直接執(zhí)行 resolver 的常規(guī)構(gòu)建邏輯。
緩存的安全性設(shè)計(jì)
unsafeCache
在webpack 4.x的構(gòu)建過(guò)程中基于timestamp比對(duì)策略的一種cache方式,它有兩個(gè)維度,resolve(解析器)的unsafeCache和module(模塊)的unsafeCache。如果同時(shí)開啟,那么從入口文件開始,webpack通過(guò)resolve規(guī)則解析所有的依賴文件,將模塊之間的依賴關(guān)系和解析后的文件內(nèi)容保存起來(lái),并存儲(chǔ)依賴的最后變更時(shí)間(timestamp),一旦發(fā)現(xiàn)相同引用,返回緩存。
而webpack 5.x版本已經(jīng)放棄了這種緩存策略,默認(rèn)只針對(duì)開啟cache選項(xiàng)并且是node_modules下的依賴才開啟unsafeCache,判斷是否有文件系統(tǒng)序列化后的文件信息來(lái)判斷是否需要重新構(gòu)建。
safeCache
模塊間的依賴關(guān)系被基于內(nèi)容對(duì)比的算法(contentHash)被記錄下來(lái),并存入到ModulGraph的class中的weakmap,相比于依賴時(shí)間戳的方式更可靠。
緩存的容量限制
除了需要考慮緩存的安全性,緩存的容量限制也不能忽視,緩存不可能無(wú)限疊加,這里就涉及到經(jīng)典的LRU cache算法(Least
Recently Used 最近最少使用)。
- 單向鏈表 添加、刪除節(jié)點(diǎn)O(1),查找O(n)
- 雙向鏈表加哈希表結(jié)合體 O(1)
LRU分析
當(dāng)存在熱點(diǎn)數(shù)據(jù)時(shí),LRU的效率很好,但偶發(fā)性的、周期性的批量操作會(huì)導(dǎo)致LRU命中率急劇下降,緩存污染情況比較嚴(yán)重。
LRU算法的改進(jìn)方案
redis使用的改進(jìn)算法LIRS、LRU-K等,感興趣的同學(xué)自行查閱
作者:丁楠。我不生產(chǎn)代碼,我是代碼的搬運(yùn)工。