將程式碼分割 JavaScript

載入大型 JavaScript 資源會大幅影響網頁速度。將 JavaScript 分割成較小的區塊,只下載在啟動期間頁面運作所需的必要項目,可大幅改善網頁的載入回應,進而改善網頁的與下一個 Paint (INP) 互動 (INP) 的情況。

隨著網頁下載、剖析及編譯大型 JavaScript 檔案,可能會長時間沒有回應。網頁元素是網頁初始 HTML 的一部分,並且由 CSS 設定樣式,因此您看得到。不過,由於提供這些互動元素所需的 JavaScript,以及網頁載入的其他指令碼,可能得剖析並執行 JavaScript,讓它們得以正常運作。結果是使用者可能會覺得互動延遲有大幅延遲,甚至是完全毀損。

這通常是因為在主執行緒上剖析及編譯 JavaScript,而導致主執行緒遭到封鎖。如果這項程序花費的時間過長,互動式頁面元素可能無法充分回應使用者輸入的內容。其中一個解決方法是僅載入正常運作所需的 JavaScript,同時透過稱為程式碼分割的技術延遲載入其他 JavaScript。本單元著重介紹這兩項技巧的後期。

透過程式碼分割功能,減少啟動期間的 JavaScript 剖析和執行作業

如果 JavaScript 執行時間超過 2 秒,Lighthouse 會擲回警告,且在執行時間超過 3.5 秒就會失敗。JavaScript 剖析和執行過多皆可能是網頁生命週期的「任何」潛在問題,因為如果使用者與頁面互動的時間與負責處理和執行 JavaScript 的主要執行緒工作相同,可能會增加互動的輸入延遲時間

此外,JavaScript 的執行和剖析過多情形在初次載入網頁期間會特別發生問題,因為這是使用者很有可能與網頁互動的頁面生命週期階段。事實上,總封鎖時間 (TBT) (載入時間指標) 與 INP 高度相關,表示使用者在初始網頁載入期間很常嘗試互動。

Lighthouse 稽核功能會回報網頁要求每個 JavaScript 檔案所花費的時間,可協助您確切找出可能用於程式碼分割的指令碼。接著,您可以使用 Chrome 開發人員工具中的涵蓋率工具進一步找出網頁載入期間使用的 JavaScript 部分。

程式碼分割是實用的技術,可減少網頁的初始 JavaScript 酬載。可讓您將 JavaScript 套件分成兩部分:

  • 載入網頁時需要的 JavaScript,因此任何時間都無法載入。
  • 其他可在稍後載入的 JavaScript,通常是在使用者與網頁上特定互動元素互動的時間點。

使用動態 import() 語法即可分割程式碼。這項語法與 <script> 元素不同,後者會在網頁生命週期的稍後要求 JavaScript 資源。

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

在上述 JavaScript 程式碼片段中,只有當使用者模糊處理任何表單的 <input> 欄位時,系統才會下載、剖析及執行 validate-form.mjs 模組。在這種情況下,負責觸發表單驗證邏輯的 JavaScript 資源只會在網頁實際使用時才會參與。

您可以設定 webpackParcelRollupesbuild 等 JavaScript 套件,只要發生在原始碼中進行動態 import() 呼叫時,就能將 JavaScript 套件分割成較小的區塊。這些工具大多會自動執行這項作業,但在特定情況下,您必須選擇採用這項最佳化作業。

程式碼分割的實用注意事項

雖然程式碼分割是減少初始頁面載入期間主執行緒爭用的有效方法,但如果您決定稽核 JavaScript 原始碼,找出程式碼分割機會,則必須注意以下事項。

盡可能使用 Bundler

開發人員在開發過程中使用 JavaScript 模組是常見的做法。這是絕佳的開發人員體驗,可改善程式碼的可讀性和可維護性。不過,將 JavaScript 模組運送至實際工作環境時,有些效能特性可能不理想。

最重要的是,您應使用 Bundler 處理及最佳化原始碼,包括您打算分割程式碼的模組。套裝組合工具不僅將最佳化套用到 JavaScript 原始碼,還非常有用,也能在平衡效能考量 (例如套件大小與壓縮比率) 之間發揮效果。雖然套件大小會提高壓縮效率,但套裝組合也會嘗試確保套件不會過大,導致套件評估作業產生長時間的工作。

建立套裝組合器也能避免透過網路傳送大量未封裝的模組時發生問題。使用 JavaScript 模組的架構通常具有大型複雜的模組樹狀結構。取消組合模組樹狀結構時,每個模組都代表一個獨立的 HTTP 要求,而如果您未封裝模組,網頁應用程式的互動情形可能會延遲。雖然可以使用 <link rel="modulepreload"> 資源提示盡早載入大型模組樹狀結構,但 JavaScript 套件仍建議使用從載入效能觀點,

不要無意間停用串流編譯

Chromium 的 V8 JavaScript 引擎提供許多立即可用的最佳化功能,可確保實際工作環境的 JavaScript 程式碼盡可能有效載入。其中一項最佳化稱為「串流編譯」,就像逐步剖析將串流至瀏覽器的 HTML 一樣,也會編譯從網路傳輸到的 JavaScript 串流區塊。

以下兩種方法可以確保 Chromium 中的網頁應用程式進行串流編譯:

  • 轉換正式版程式碼,避免使用 JavaScript 模組。套裝組合程式可根據編譯目標轉換 JavaScript 原始碼,目標通常是特定環境的專屬資訊。V8 會將串流編譯套用至未使用模組的任何 JavaScript 程式碼,而且您可以設定 Bundler,將 JavaScript 模組程式碼轉換為不使用 JavaScript 模組及其功能的語法。
  • 如要將 JavaScript 模組推送至實際工作環境,請使用 .mjs 擴充功能。無論實際工作環境的 JavaScript 是否使用模組,針對使用模組的 JavaScript 都沒有特殊的「內容類型」,而 JavaScript 就不會使用此類內容。在 V8 有疑慮的情況下,當您使用 .js 擴充功能在實際工作環境中提供 JavaScript 模組時,能有效選擇停用串流編譯。如果您為 JavaScript 模組使用 .mjs 擴充功能,V8 可確保未中斷模組 JavaScript 程式碼的串流編譯。

千萬別因為這些注意事項而讓您擺脫使用程式碼分割的問題。程式碼分割是減少向使用者顯示初始 JavaScript 酬載的有效方法,但只要使用整合器並瞭解如何保留 V8 的串流編譯行為,就能確保實際工作環境 JavaScript 程式碼盡可能加快使用者速度。

動態匯入示範

Webpack

webpack 隨附名為 SplitChunksPlugin 的外掛程式,可讓您設定 Bundler 分割 JavaScript 檔案的方式。Webpack 可辨識動態 import() 和靜態 import 陳述式。如要修改 SplitChunksPlugin 的行為,您可以在設定中指定 chunks 選項:

  • chunks: async 是預設值,指的是動態 import() 呼叫。
  • chunks: initial 是指靜態 import 呼叫。
  • chunks: all 包含動態 import() 和靜態匯入,讓您能夠在 asyncinitial 匯入作業之間共用區塊。

根據預設,每當 Webpack 遇到動態 import() 陳述式時,都會為該模組建立單獨的區塊:

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

上述程式碼片段的預設 Webpack 設定會產生兩個不同的區塊:

  • main.js 區塊 (Webpack 分類為 initial 區塊),包含 main.js./my-function.js 模組。
  • async 區塊,其中只包含 form-validation.js (如果已設定,會在資源名稱中包含檔案雜湊)。只有在 condition 為「truthy」時,系統才會下載這個區塊。

這項設定可讓您延遲載入 form-validation.js 區塊,直到實際需要為止。這樣可以減少初始網頁載入期間的指令碼評估時間,藉此提升載入回應速度。符合指定條件時,系統會下載 form-validation.js 區塊的指令碼並進行評估。在這種情況下,系統會下載動態匯入的模組。例如只針對特定瀏覽器下載 polyfill 的情況;或是如前例所示,使用者互動需要匯入的模組。

另一方面,將 SplitChunksPlugin 設定變更為指定 chunks: initial,可確保程式碼只會分割至初始區塊。這些區塊包括靜態匯入的區塊,或列於 Webpack 的 entry 屬性中。在上述範例中,產生的區塊會是單一指令碼檔案中的 form-validation.js「和」main.js 組合,進而導致初始頁面載入效能降低。

您也可以設定 SplitChunksPlugin 的選項,將較大的指令碼分割成多個較小的指令碼,例如使用 maxSize 選項,指示 Webpack 將超過 maxSize 指定的區塊分割為個別檔案。將大型指令碼檔案分成較小的檔案可以改善載入回應速度,因為在某些情況下,會耗用大量 CPU 的指令碼評估工作會分割成較小的工作,因為這樣可能會長時間封鎖主執行緒。

此外,產生較大的 JavaScript 檔案也表示指令碼更有可能因快取無效而受到影響。舉例來說,如果您同時提供含有架構和第一方應用程式程式碼的大型指令碼,則如果只有更新架構,組合資源中就不會有其他內容,因此整個組合都會失效。

另一方面,較小的指令碼檔案會提高回訪者從快取擷取資源的可能性,進而加快重複造訪的網頁載入速度。但是,相較於大型檔案,較小的檔案因為壓縮後較不容易獲益,並且可能會使用未修剪的瀏覽器快取,增加網頁載入時的網路封包往返時間。請務必小心,在快取效率、壓縮效率和指令碼評估時間之間取得平衡。