webpack 5 之持久化緩存


Opt-in

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

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

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

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

什么是緩存失效?webpack 需要確認 entry 的緩存何時會失效,並在失效時不再將其用於構建。因此,當你應用程序修改文件時,就會發生此情況。

示例:修改 magic.js。webpack 必須讓 entry 為 magic.js 的緩存失效。構建將重新處理該文件,即運行 babel,typescript 諸如此類工具,重新解析文件並運行代碼生成。webpack 可能還會致使 entry 為 bundle.js 的緩存失效。然后根據原模塊重新構建此文件。

為此,webpack 追蹤了每個模塊的 fileDependencies contextDependencies 以及 missingDependencies,並創建了文件系統快照。此快照會與真實文件系統進行比較,當檢測到差異時,將觸發對應模塊的重新構建。

webpack 給 bundle.js 的緩存 entry 設置了一個 etag,它為所有貢獻者的 hash 值。比較這個 etag,只有當它與緩存 entry 匹配時才能使用。

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

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

  • 當 npm 升級 loader 或 plugin 時

  • 當更改配置時

  • 當更改在配置中讀取的文件時

  • 當 npm 升級配置中使用的 dependencies 時

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

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

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

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

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

構建依賴(Build dependencies)

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

值類型有兩種:文件和目錄。目錄類型必須以斜杠(/)結尾。其他所有內容都解析為文件類型。

對於目錄類型來說,會解析其最近的 package.json 中的 dependencies。對於文件類型來說,我們將查看 node.js 模塊緩存以尋找其依賴。

示例:構建通常取決於 webpack 本身的 lib 文件夾:你可以這樣配置:

cache.buildDependencies: {
    defaultWebpack: ["webpack/lib/"]
}

當 webpack/lib 或 webpack 依賴的庫(如,watchpackenhanced-resolved 等)發生任何變化時,其緩存將失效。webpack/lib 已是默認值,默認情況下無需配置。

另一個示例:構建依舊取決於你的配置文件。具體配置如下:

cache.buildDependencies: {
    config: [__filename]
}

__filename 變量指向 node.js 中的當前文件。

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

如果配置文件通過 fs.readFile 讀取文件,則將不會成為構建依賴項,因為 webpack 僅遵循 require()。你需要手動將此類文件添加到 buildDependencies 中。

緩存版本(Version)

構建的某些依賴項不能單純的依靠對文件的引用,如,從數據庫讀取的值,環境變量或命令行上傳遞的值。對於這些值,我們給出了新的配置項 cache.version

cache.version 類型為 string。傳遞不同的字符串將使持久化緩存失效。

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

cache: {
    version: `${process.env.GIT_REV}`
}

緩存名(Name)

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

cache.name 類型為 string。傳遞值將創建一個隔離且獨立的持久化緩存。

cache.name 被用於對文件名進行持久化緩存。確保僅傳遞短小且 fs-safe 的名稱。

示例:你的配置可以使用 --env.target mobile|desktop 參數為移動端或 PC 用戶創建不同的構建。具體配置如下:

cache: {
    name: `${env.target}`
}

性能優化

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

此優化將用於配置項 cache.managedPaths 中的所有 path。它默認為 webpack 安裝了 node_modules 目錄。

啟用此優化后,請勿手動編輯 node_modules。你可以使用 cache.managedPaths: [] 禁用它。

當使用 Yarn PnP 時,將啟用另一個優化。由於緩存內容不可變,yarn 緩存中的所有文件都將完全跳過哈希和時間戳的操作(甚至不會追蹤 version 和 name)。

此操作由配置項 cache.immutablePaths 控制。啟用 Yarn PnP 時,默認為安裝了 webpack 的 yarn 緩存。

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

使用持久化緩存

確保你已閱讀並理解以上信息!

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

cache: {
    type: "filesystem",
    buildDependencies: {
        config: [ __filename ] // 當你 CLI 自動添加它時,你可以忽略它
    }
}

Watching

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

當設置 cache.type: "filesystem" 時,webpack 會在內部以分層方式啟用文件系統緩存和內存緩存。從緩存讀取時,會先查看內存緩存,如果內存緩存未找到,則降級到文件系統緩存。寫入緩存將同時寫入內存緩存和文件系統緩存。

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

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

  • Loading cache

  • Building

  • Emitting

  • Display results (stats)

  • Persisting cache (if changed)

  • Process exits

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

  • Loading cache

  • Building

  • Emitting

  • Display results (stats)

  • Attach filesystem watchers

  • Wait cache.idleTimeoutForInitialStore

  • Persisting cache (if changed)

  • On change:

    • Building

    • Emitting

    • Display results (stats)

    • Wait cache.idleTimeout

    • Persisting cache (if changed)

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

錯誤處理

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

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


Details

正常使用不需要以下信息。

使用 webpack 的高級工具指南

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

CLI 指南

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

  • 默認情況下,CLI 會將 cache.buildDependencies.defaultConfig 設置為所用的配置文件

  • CLI 會將命令行參數附加到 cache.version

  • 使用命令行參數時,CLI 可能會在 cache.name 中添加注釋。

調試信息

使用如下配置,將輸出額外的調試信息:

infrastructureLogging: {
    debug: /webpack\.cache/
}

 


內部工作流
  • webpack 讀取緩存文件。

    • 沒有緩存文件 -> 未構建緩存

    • 緩存文件中的 version 與 cache.version 不匹配 -> 沒有構建緩存

  • webpack 將解析快照(resolve snapshot)與文件系統進行對比

    • 匹配到 -> 繼續后續流程

    • 沒有匹配到:

      • 再次解析所有解析結果(resolve results

        • 沒有匹配到 -> 未構建緩存

        • 匹配到 -> 繼續后續流程

  • webpack 將構建依賴快照(build dependencies snapshot)與文件系統進行對比

    • 沒有匹配到 -> 未構建緩存

    • 匹配到 -> 繼續后續流程

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

  • 構建運行(有緩存或沒有緩存)

    • 追蹤構建依賴關系

      • 追蹤 cache.buildDependencies

      • 追蹤已使用的 loader

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

    • 解析依賴關系已追蹤

    • 解析結果已追蹤

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

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

  • 持久化緩存文件序列化到磁盤

序列化

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

webpack.util.serialization.register(Constructor, request, name, serializer);

 

Constructor 應為一個 class 或構造器函數。對於任何需要序列化的對象的 object.constructor 將被用於查找序列化器(serializer)。

request 將被用於加載調用 register 模塊。它應指向當前模塊。它將以這種方式使用:require(request)

name 被用於區分具有相同 request 的多個 register 調用。

serializer 是至少擁有 serialize 和 deserialize 兩個方法的對象。

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

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

serialize 和 deserialize 應以相同的順序讀取和寫入相同的對象。

示例:

// some-module/lib/MyClass.js
class MyClass {
    constructor(a, b) {
        this.a = a;
        this.b = b;
        this.c = undefined;
    }
}

register(MyClass, "some-module/lib/MyClass", null, {
    seralize(obj, { write }) {
        write(obj.a);
        write(obj.b);
        write(obj.c);
    }
    deserialize({ read }) {
        const obj = new MyClass(read(), read());
        obj.c = read();
        return obj;
    }
});

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

原文出自https://mp.weixin.qq.com/s/sPb20xx-I64mifKa2N3YFQ

喜歡這篇文章?歡迎打賞~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM