天天看點

webpack 5 之持久化緩存

Opt-in

首先,要注意的是預設情況下不會啟用持久化緩存。你可以自行選擇啟用。

為何如此?webpack 旨在注重建構安全而非性能。我們沒有打算預設啟用這一功能,主要原因在于此功能雖然有 95% 幾率提升性能,但仍有 5% 的幾率中斷你的應用程式/工作流/建構。

這可能聽起來很糟,但相信我它并非如此。隻不過需要開發人員進行額外的操作來配置它。

序列化與反序列化功能具有無需配置的開箱即用體驗,但開箱即用的部分可能緻使緩存失效。

什麼是緩存失效?webpack 需要确認 entry 的緩存何時會失效,并在失效時不再将其用于建構。是以,當你應用程式修改檔案時,就會發生此情況。

示例:修改 ​<code>​magic.js​</code>​。webpack 必須讓 entry 為 ​<code>​magic.js​</code>​ 的緩存失效。建構将重新處理該檔案,即運作 babel,typescript 諸如此類工具,重新解析檔案并運作代碼生成。webpack 可能還會緻使 entry 為 ​<code>​bundle.js​</code>​ 的緩存失效。然後根據原子產品重新建構此檔案。

為此,webpack 追蹤了每個子產品的 ​<code>​fileDependencies​</code>​ ​<code>​contextDependencies​</code>​ 以及 ​<code>​missingDependencies​</code>​,并建立了檔案系統快照。此快照會與真實檔案系統進行比較,當檢測到差異時,将觸發對應子產品的重新建構。

webpack 給 ​<code>​bundle.js​</code>​ 的緩存 entry 設定了一個 ​<code>​etag​</code>​,它為所有貢獻者的 hash 值。比較這個 ​<code>​etag​</code>​,隻有當它與緩存 entry 比對時才能使用。

webpack 4 中的記憶體緩存也依賴上述這些。從開發人員角度來說,這些都能夠開箱即用,無需額外配置。但對于 webpack 5 的持久化緩存來說,卻充滿着挑戰。

以下操作均會讓 webpack 使 entry 緩存失效:

當 npm 更新 loader 或 plugin 時

當更改配置時

當更改在配置中讀取的檔案時

當 npm 更新配置中使用的 dependencies 時

當不同指令行參數傳遞給 build 腳本時

當有自定義建構腳本并進行更改時

這變得非常棘手。開箱即用的情況下,webpack 無法處理所有這些情況。這就是我們為什麼選擇安全的方式,并将持久化緩存變為可選特性的原因。我們希望讀者可以學習如何啟用持久化緩存,以為你提供正确的提示。我們希望你知道需要使用哪種配置來處理你自定義的建構腳本。

建構依賴(dependencies),緩存版本(version)和緩存名(name)

為了處理建構過程中的依賴關系,webpack 提供了三個新工具:

此為全新的配置項 ​<code>​cache.buildDependencies​</code>​,它可以指定建構過程中的代碼依賴。為了使它更簡易,webpack 負責解析并遵循配置值的依賴。

值類型有兩種:檔案和目錄。目錄類型必須以斜杠(​<code>​/​</code>​)結尾。其他所有内容都解析為檔案類型。

對于目錄類型來說,會解析其最近的 ​<code>​package.json​</code>​ 中的 dependencies。對于檔案類型來說,我們将檢視 node.js 子產品緩存以尋找其依賴。

示例:建構通常取決于 webpack 本身的 lib 檔案夾:你可以這樣配置:

當 ​<code>​webpack/lib​</code>​ 或 webpack 依賴的庫(如,​<code>​watchpack​</code>​,​<code>​enhanced-resolved​</code>​ 等)發生任何變化時,其緩存将失效。​<code>​webpack/lib​</code>​ 已是預設值,預設情況下無需配置。

另一個示例:建構依舊取決于你的配置檔案。具體配置如下:

​<code>​__filename​</code>​ 變量指向 node.js 中的目前檔案。

當配置檔案或配置檔案中通過 ​<code>​require​</code>​ 依賴的任何内容發生更改時,也會使得持久化緩存失效。當配置檔案通過 ​<code>​require()​</code>​ 引用了所有使用過的插件時,它們也會成為建構依賴項。

如果配置檔案通過 ​<code>​fs.readFile​</code>​ 讀取檔案,則将不會成為建構依賴項,因為 webpack 僅遵循 ​<code>​require()​</code>​。你需要手動将此類檔案添加到 ​<code>​buildDependencies​</code>​ 中。

建構的某些依賴項不能單純的依靠對檔案的引用,如,從資料庫讀取的值,環境變量或指令行上傳遞的值。對于這些值,我們給出了新的配置項 ​<code>​cache.version​</code>​。

​<code>​cache.version​</code>​ 類型為 string。傳遞不同的字元串将使持久化緩存失效。

示例:你的配置中可能會讀取環境變量中的 ​<code>​GIT_REV​</code>​ 并将其與 ​<code>​DefinePlugin​</code>​ 一起使用以将其嵌入到 bundle 中。這使得 ​<code>​GIT_REV​</code>​ 成為你建構的依賴項。具體配置如下:

在某些情況下,依賴關系會在多個不同的值間切換,并且對于每個值更改都會使得持久化緩存失效,這顯然是浪費資源的。對于這類值,我們給出了新的配置項 ​<code>​cache.name​</code>​。

​<code>​cache.name​</code>​ 類型為 string。傳遞值将建立一個隔離且獨立的持久化緩存。

​<code>​cache.name​</code>​ 被用于對檔案名進行持久化緩存。確定僅傳遞短小且 fs-safe 的名稱。

示例:你的配置可以使用 ​<code>​--env.target mobile|desktop​</code>​ 參數為移動端或 PC 使用者建立不同的建構。具體配置如下:

性能優化

對大部分 node_modules 進行哈希處理并加蓋時間戳以生存建構和正常依賴項,其代價非常昂貴,并且還會大大降低 webpack 的執行速度。為避免這種情況出現,webpack 引入了相關的性能優化,預設情況下會跳過 ​<code>​node_modules​</code>​,并使用 ​<code>​package.json​</code>​ 中的 ​<code>​version​</code>​ 和 ​<code>​name​</code>​ 作為資料源。

此優化将用于配置項 ​<code>​cache.managedPaths​</code>​ 中的所有 path。它預設為 webpack 安裝了 ​<code>​node_modules​</code>​ 目錄。

啟用此優化後,請勿手動編輯 ​<code>​node_modules​</code>​。你可以使用 ​<code>​cache.managedPaths: []​</code>​ 禁用它。

當使用 Yarn PnP 時,将啟用另一個優化。由于緩存内容不可變,yarn 緩存中的所有檔案都将完全跳過哈希和時間戳的操作(甚至不會追蹤 ​<code>​version​</code>​ 和 ​<code>​name​</code>​)。

此操作由配置項 ​<code>​cache.immutablePaths​</code>​ 控制。啟用 Yarn PnP 時,預設為安裝了 webpack 的 yarn 緩存。

不要手動編輯 yarn 緩存,因為這根本不可行。

使用持久化緩存

確定你已閱讀并了解以上資訊!

此為啟用持久化緩存的典型配置:

持久化緩存可用于單獨建構和連續建構(watch)。

當設定 ​<code>​cache.type: "filesystem"​</code>​ 時,webpack 會在内部以分層方式啟用檔案系統緩存和記憶體緩存。從緩存讀取時,會先檢視記憶體緩存,如果記憶體緩存未找到,則降級到檔案系統緩存。寫入緩存将同時寫入記憶體緩存和檔案系統緩存。

檔案系統緩存不會直接将對磁盤寫入的請求進行序列化。它将等到編譯過程完成且編譯器處于空閑狀态才會執行。如此處理的原因是序列化和磁盤寫入會占用資源,并且我們不想額外延遲編譯過程。

針對單一建構,其工作流為:

Loading cache

Building

Emitting

Display results (stats)

Persisting cache (if changed)

Process exits

針對連續建構(watch),其工作流為:

Attach filesystem watchers

Wait <code>cache.idleTimeoutForInitialStore</code>

On change:

Wait <code>cache.idleTimeout</code>

你會發現兩個新的配置項 ​<code>​cache.idleTimeout​</code>​ 和 ​<code>​cache.idleTimeoutForInitialStore​</code>​,它們控制着持久化緩存之前編譯器必須空閑的時長。​<code>​cache.idleTimeout​</code>​ 預設為 60s,​<code>​cache.idleTimeoutForInitialStore​</code>​ 預設為 0s。由于序列化阻止了事件循環,是以在序列化緩存時不進行緩存檢測。此延遲嘗試避免由于快速編輯檔案,而在 watch 模式下導緻重新編譯造成的延遲,同時嘗試為下一次冷啟動保持持久化緩存的最新狀态。這是一個折中的解決方案,可以設定适合你工作流的值。較小的值會縮短冷啟動時間,但會增加延遲重新建構的風險。

發生錯誤要恢複持久化緩存的方式,可以通過删除整個緩存并進行全新的建構,或者通過删除有問題的緩存 entry 并使得該項目保持未緩存狀态來進行。

在這種情況下,webpack 的 logger 會發出警告。欲了解更多,請參閱 ​<code>​infrastructureLogging​</code>​ 的配置項。

Details

正常使用不需要以下資訊。

封裝 webpack 的工具可以選擇其他預設值。當不允許使用自定義擴充的 webpack 時,由于可以完全控制所有建構的依賴項,是以可以預設打開持久化存儲。

預設情況下,使用 webpack 的 CLI 可能會添加一些建構依賴關系,而 webpack 本身不會。

預設情況下,CLI 會将 <code>cache.buildDependencies.defaultConfig</code> 設定為所用的配置檔案

CLI 會将指令行參數附加到 <code>cache.version</code>

使用指令行參數時,CLI 可能會在 <code>cache.name</code> 中添加注釋。

使用如下配置,将輸出額外的調試資訊:

webpack 讀取緩存檔案。

沒有緩存檔案 -&gt; 未建構緩存

緩存檔案中的 <code>version</code> 與 <code>cache.version</code> 不比對 -&gt; 沒有建構緩存

webpack 将解析快照(<code>resolve snapshot</code>)與檔案系統進行對比

比對到 -&gt; 繼續後續流程

沒有比對到:

再次解析所有解析結果(<code>resolve results</code>)

沒有比對到 -&gt; 未建構緩存

webpack 将建構依賴快照(<code>build dependencies snapshot</code>)與檔案系統進行對比

對緩存 entry 進行反序列化(在建構過程中對較大的緩存 entry 進行延遲反序列化)

建構運作(有緩存或沒有緩存)

追蹤建構依賴關系

追蹤 <code>cache.buildDependencies</code>

追蹤已使用的 loader

新的建構依賴關系已解析完成

解析依賴關系已追蹤

解析結果已追蹤

建立來自所有新解析依賴項的快照

建立來自所有新建構依賴項的快照

持久化緩存檔案序列化到磁盤

所有支援序列化的 class 都需要注冊一個序列化器:

​<code>​Constructor​</code>​ 應為一個 class 或構造器函數。對于任何需要序列化的對象的 ​<code>​object.constructor​</code>​ 将被用于查找序列化器(serializer)。

​<code>​request​</code>​ 将被用于加載調用 ​<code>​register​</code>​ 子產品。它應指向目前子產品。它将以這種方式使用:​<code>​require(request)​</code>​。

​<code>​name​</code>​ 被用于區分具有相同 ​<code>​request​</code>​ 的多個 ​<code>​register​</code>​ 調用。

​<code>​serializer​</code>​ 是至少擁有 ​<code>​serialize​</code>​ 和 ​<code>​deserialize​</code>​ 兩個方法的對象。

當需序列化對象時,請調用 ​<code>​serializer.serialize(object, context)​</code>​。​<code>​context​</code>​ 是至少擁有一個 ​<code>​write(anything)​</code>​ 方法的對象 此方法将内容寫入輸出流。傳遞的值也會被序列化。

當需要反序列化對象時,請調用 ​<code>​serializer.deserialize(context)​</code>​。​<code>​context​</code>​ 是至少擁有一個 ​<code>​read(): anything​</code>​ 方法的對象。此方法會反序列化輸入流中的某些内容。​<code>​deserialize​</code>​ 必須傳回反序列化後的對象。

​<code>​serialize​</code>​ 和 ​<code>​deserialize​</code>​ 應以相同的順序讀取和寫入相同的對象。

示例:

基本資料類型和引用資料類型的序列化器都已被注冊,即 string,number,Array,Set,Map,RegExp,plain objects,Error。

繼續閱讀