指令碼評估和長時間任務

載入指令碼時,瀏覽器需要時間先評估指令碼,再執行評估作業,因此可能會造成長時間的工作。瞭解指令碼評估的運作方式,以及您可以採取哪些措施來避免指令碼在頁面載入時造成較長的工作。

在最佳化與下一個繪製互動 (INP) 互動時,你看到的大部分建議是自行最佳化互動。例如,最佳化長時間工作指南中的技巧包括透過 setTimeoutisInputPending 產生成效等技巧。這些技巧很有用,因為它們可以避免長時間工作,讓主執行緒有部分呼吸的空間,可以較快執行互動和其他活動,而不必等待執行長時間的工作。

不過,如果自行載入指令碼,則需要花費較長的工作時間呢?這些工作可能會幹擾使用者互動,並在載入期間影響網頁的 INP。本指南將探討瀏覽器如何處理由指令碼評估啟動的工作,並探討您可以對指令碼評估工作執行哪些操作,讓主要執行緒在網頁載入時,能夠回應使用者輸入內容。

什麼是指令碼評估?

如果您剖析了傳送大量 JavaScript 的應用程式,您可能曾在長時間工作標示為「評估指令碼」

Chrome 開發人員工具的效能分析器會以視覺化的方式呈現指令碼評估作業。該工作在啟動期間會執行長時間的工作,導致主執行緒回應使用者互動的能力遭到封鎖。
Chrome 開發人員工具的效能分析器會顯示指令碼評估作業,在此情況下,工作已足以造成長時間工作導致主執行緒無法完成其他工作,包括促成使用者互動的工作。

在瀏覽器中執行 JavaScript 時,指令碼評估是必要環節,因為 JavaScript 會在執行前準時進行。評估指令碼時,系統會先剖析指令碼是否有錯誤。如果剖析器沒有找到錯誤,指令碼便會編譯成 bytecode,然後繼續執行。

雖然指令碼評估可能有問題,因為使用者可能會在網頁最初顯示後不久與網頁互動。但是,即使網頁「已轉譯」,也不代表網頁已完成載入。網頁正在檢索指令碼,因此載入期間的互動可能會延遲。雖然無法保證在這個時間點會進行指定的互動,但負責的指令碼可能尚未載入,但有可能是互動依附於「已就緒」的 JavaScript,或是互動並非依附於 JavaScript。

指令碼與評估這些指令碼的工作之間的關係

如何啟動指令碼評估工作,取決於目前載入的指令碼是透過一般 <script> 元素載入,或指令碼是否為透過 type=module 載入的模組。瀏覽器處理事情的方式各不相同,因此主要瀏覽器引擎的指令碼評估行為也會因瀏覽器而異。

使用 <script> 元素載入指令碼

分派用於評估指令碼的工作數量,通常與網頁上的 <script> 元素數量有直接關聯。每個 <script> 元素都會啟動一項工作,用於評估要求的指令碼,以便加以剖析、編譯和執行。如果是以 Chromium 為基礎的瀏覽器、Safari Firefox,使用者就會採取這種做法。

這一點的重要性在於,假設您使用 Bundler 管理實際運作指令碼,且將要執行的所有網頁都設為單一指令碼集,如果您的網站出現這種情況,會分派單一工作來評估該指令碼。這是不好的事情嗎?不一定,除非該指令碼龐大

如要避免指令碼評估工作,您可以避免載入大量 JavaScript 區塊,並運用額外的 <script> 元素載入更個別且較小的指令碼。

雖然載入網頁期間請盡可能減少 JavaScript 的載入,但分割指令碼可確保,不會因為執行一項可能封鎖主執行緒的大型工作,而會產生大量不封鎖主執行緒的小型工作,或至少低於啟動啟動工作。

多項涉及指令碼評估的工作,會顯示在 Chrome 開發人員工具的效能分析器中。由於載入多個較小的指令碼,而非較大型的指令碼,因此工作較不可能轉為長篇工作,讓主執行緒更快回應使用者輸入內容。
網頁 HTML 中含有多個 <script> 元素後,會產生多項工作來評估指令碼。比起傳送一個大型指令碼組合給使用者,較有可能會封鎖主執行緒。

您可以將用來評估指令碼的工作分拆,就像在互動期間執行的事件回呼期間產生類似作用。然而,在指令碼評估中,產生機制會將您載入的 JavaScript 分成多個較小的指令碼,而不是比封鎖主執行緒的可能性更大的較小指令碼。

載入含有 <script> 元素和 type=module 屬性的指令碼

現在可以使用 <script> 元素上的 type=module 屬性,在瀏覽器中以原生方式載入 ES 模組。這種載入指令碼的做法對開發人員有益處,例如不必轉換程式碼供正式環境使用 (尤其是與匯入地圖搭配使用時)。不過,這種載入指令碼的方式會安排因瀏覽器而異的工作。

以 Chromium 為基礎的瀏覽器

在 Chrome 等瀏覽器 (或源自 Chrome 瀏覽器) 中,使用 type=module 屬性載入 ES 模組後,產生的工作方式與不使用 type=module 時的一般工作不同。舉例來說,每個模組指令碼的工作都會執行,其中包含標示為「Compile module」的活動。

模組編譯作業適用於多種工作,如 Chrome 開發人員工具中的圖表。
以 Chromium 為基礎的瀏覽器中的模組載入行為。每個模組指令碼都會產生 Compile 模組呼叫,以便在評估前編譯內容。

模組編譯完成後,後續在模組中執行的任何程式碼都會啟動標示為「評估模組」的活動。

Chrome 開發人員工具的效能面板以視覺化方式呈現,及時評估模組。
當模組中的程式碼執行時,系統會及時評估該模組。

這對 Chrome 和相關瀏覽器的影響至少是,使用 ES 模組時編譯步驟遭到分解。對於管理長時間工作來說,可說是大獲成功。不過,產生的模組評估工作還是意味著您會產生一些無可避免的成本。雖然建議您盡量少建構 JavaScript,但無論使用哪種瀏覽器,都必須使用 ES 模組,具備下列優點:

  • 所有模組程式碼都會自動在嚴格模式下執行,讓 JavaScript 引擎得以進行潛在最佳化調整,但這類模式無法在嚴格環境中進行最佳化調整。
  • 根據預設,系統會將使用 type=module 載入的指令碼視為延遲。您可以在透過 type=module 載入的指令碼上使用 async 屬性,以便變更這項行為。

Safari 和 Firefox

在 Safari 和 Firefox 中載入模組時,每個模組都會以獨立的工作進行評估。也就是說,理論上您可以載入一個頂層模組,其中僅包含其他模組的 靜態 import 陳述式,且每個載入的模組都會產生個別的網路要求和評估工作。

使用動態 import() 載入指令碼

動態 import() 是載入指令碼的另一種方法。與必須在 ES 模組頂端顯示的靜態 import 陳述式不同,動態 import() 呼叫可顯示在指令碼中的任何位置,視需要載入 JavaScript 區塊。這項技巧稱為「程式碼分割」

動態 import() 在改善 INP 方面有兩項優點:

  1. 延遲載入模組的模組可減少啟動時載入的 JavaScript,有助於減少啟動期間主要執行緒的爭用情形。這麼做可釋放主執行緒,使其可以更回應使用者互動。
  2. 發出動態 import() 呼叫時,每個呼叫都會有效將每個模組的編譯和評估作業分離其各自的工作。當然,載入超大型模組的動態 import() 會啟動一個大型的指令碼評估工作,而且如果互動和動態 import() 呼叫同時發生,可能會幹擾主執行緒回應使用者輸入內容的能力。因此,您仍然必須盡量減少 JavaScript。

動態 import() 呼叫在所有主要瀏覽器引擎中的運作方式都類似:產生的指令碼評估工作和動態匯入的模組數量相同。

在網路工作站中載入指令碼

網路工作站是特殊的 JavaScript 用途。網路工作站會在主執行緒上註冊,而工作站中的程式碼會在其執行緒上執行。這在邏輯上非常實用,雖然註冊 Web 工作站的程式碼是在主執行緒上執行,但網路工作站內的程式碼卻則不需要。這樣可以減少主執行緒壅塞,同時讓主要執行緒更回應使用者互動。

除了減少主執行緒工作之外,網路工作站「本身」也能透過支援模組工作站的瀏覽器中,透過 importScripts 或靜態 import 陳述式載入要在工作站環境中使用的外部指令碼。如此一來,網路工作站要求的所有指令碼都會從主執行緒中評估。

取捨與注意事項

將指令碼拆成數個獨立檔案,較小型的檔案能夠減少耗時的工作,而不是要載入較少但較大的檔案。決定如何切割指令碼時,請務必考量以下因素。

壓縮效率

壓縮是分割指令碼的因素。如果指令碼較小,壓縮效率會稍微降低。指令碼越大,壓縮效果就越好。雖然提高壓縮效率有助於盡量降低指令碼的載入時間,但要確保將指令碼分割為足夠的小區塊,藉此促進啟動期間的互動性。

軟體套件是管理網站所需指令碼輸出大小的工具:

  • 對 Webpack 有疑慮時,其 SplitChunksPlugin 外掛程式可助您一臂之力。請參閱 SplitChunksPlugin 說明文件,瞭解您可以設定哪些選項來管理素材資源大小。
  • 如果是 Rollupesbuild 等其他套件,您可以在程式碼中使用動態 import() 呼叫來管理指令碼檔案大小。這些套件 (以及 Webpack) 會自動將動態匯入的素材資源分割到自己的檔案中,避免產生較大的初始套件大小。

快取撤銷

快取撤銷對網頁在重複訪客造訪時的載入速度影響重大。傳送大型單體式指令碼套裝組合時,可能會遇到瀏覽器快取功能的缺點。這是因為如果透過更新套件或修正運送錯誤修正更新第一方程式碼,整個套裝組合就會失效,必須重新下載。

分割指令碼不僅使指令碼評估工作拆解更小的工作,也提高回訪訪客從瀏覽器快取擷取更多指令碼的可能性,而非網路。這可提升整體網頁的載入速度。

巢狀模組和載入效能

如果要在實際工作環境中運送 ES 模組,並使用 type=module 屬性載入模組,請務必瞭解模組巢狀結構對啟動時間的影響。模組巢狀結構是指 ES 模組以靜態方式匯入另一個 ES 模組時,會以靜態方式匯入另一個 ES 模組的情況:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

如果 ES 模組未組合在一起,上述程式碼會產生網路要求鏈結:當從 <script> 元素要求 a.js 時,系統會分派另一個網路要求給 b.js,並涉及 c.js 的「另一個」要求。如要避免這種情況,其中一種方法是使用 Bundler,但請務必設定 Bundler,以便分離指令碼,以便分散指令碼評估工作。

如果不想使用 Bundler,則可使用 modulepreload 資源提示來規避巢狀模組呼叫,此方法會預先預先載入 ES 模組,以避免網路要求鏈結。

結語

在瀏覽器中最佳化指令碼評估,一點也不困難。具體做法視網站的需求和限製而定。不過,透過分割指令碼,您將指令碼評估工作分散在許多小型工作上,因此可讓主執行緒更有效率地處理使用者互動,不必封鎖主執行緒。

總而言之,以下是分解大型指令碼評估工作的方法:

  • 使用 <script> 元素載入不含 type=module 屬性的指令碼時,請避免載入非常大的指令碼,因為這些指令碼會啟動會封鎖主執行緒且會耗用大量資源的指令碼評估工作。將指令碼分散在更多 <script> 元素上,即可分解這項工作。
  • 使用 type=module 屬性在瀏覽器中原生載入 ES 模組時,系統會啟動每個獨立模組指令碼的評估工作。
  • 使用動態 import() 呼叫縮減初始套件的大小。這也適用於套裝組合,因為 Bundler 會將每個動態匯入的模組視為「分割點」,進而為每個動態匯入的模組產生獨立的指令碼。
  • 請務必衡量壓縮效率和快取撤銷等權衡取捨。較大型的指令碼會經過壓縮,但較有可能在較少的工作下進行較昂貴的指令碼評估工作,並導致瀏覽器快取無效,進而降低整體快取效率。
  • 如果原生使用 ES 模組而不建立套裝組合,請使用 modulepreload 資源提示,在啟動期間最佳化模組的載入作業。
  • 一如以往,請盡可能減少 JavaScript 的發布數量。

您可以取得平衡點,不過只要透過動態 import() 拆分指令碼及減少初始酬載,就能提升啟動效能,並在重要的啟動期間,進一步因應使用者互動。這可協助您在 INP 指標上獲得更高的分數,藉此提供更優質的使用者體驗。

Markus Spiske 撰寫的「Unsplash」主頁橫幅。