指令碼評估和長時間任務

載入指令碼時,瀏覽器需要時間先評估後再執行,因此會長時間執行工作。瞭解指令碼評估的運作方式,以及如何避免在網頁載入期間造成長時間工作的不便。

在最佳化與下一個顯示內容的互動 (INP) 方面,您通常會遇到最建議採取的行動,就是只想要最佳化互動。舉例來說,請參閱最佳化長期工作指南,討論使用 setTimeout 產生這類技術等技巧。這些技術可以帶來益處,因為它們會避免長時間工作,讓主執行緒增加一些呼吸空間,進而讓更多機會更快執行互動和其他活動,不必等待進行長時間的工作。

不過,如果長時間工作需要自行載入指令碼,該怎麼辦?這些工作會幹擾使用者互動,並影響網頁載入時的 INP。本指南將說明瀏覽器如何處理指令碼評估啟動的工作,並探討您可以採取哪些行動來中斷指令碼評估工作,以便在載入網頁時,讓主執行緒更能回應使用者輸入內容。

什麼是指令碼評估?

如果您剖析的應用程式會部署大量 JavaScript,您可能已看過長時間工作,其中犯罪分子標有「評估指令碼」標籤。

在 Chrome 開發人員工具的效能分析器中,指令碼評估作業會以視覺化的方式呈現。此工作會在啟動期間造成長時間的工作,阻斷主執行緒回應使用者互動的能力。
指令碼評估工作,如 Chrome 開發人員工具的效能分析器所示。在這種情況下,該工作就已足夠造成長時間的工作造成主執行緒無法執行其他工作,包括促成使用者互動的工作。

指令碼評估是在瀏覽器中執行 JavaScript 的必要部分,因為 JavaScript 是在執行前的及時編譯。評估指令碼時,系統會先剖析指令碼是否有錯誤。如果剖析器沒有找到錯誤,指令碼就會編譯為位元碼,然後繼續執行。

必要時,使用者可能會嘗試在網頁初次轉譯後不久就與網頁互動,因此這可能會產生問題評估。不過,即使網頁「已轉譯」,也不代表頁面已完成載入,載入期間的互動可能會延遲,因為網頁正忙於評估指令碼。雖然我們無法保證可在這個時間點進行互動 (由於負責該指令碼的指令碼可能尚未載入),但互動可能必須在「已經」準備就緒,或是互動功能完全不需依賴 JavaScript 的情況下運作。

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

負責執行指令碼評估的工作如何啟動,取決於載入的指令碼是以一般 <script> 元素載入,還是指令碼是透過 type=module 載入。瀏覽器處理指令碼的方式往往不同,因此當主要瀏覽器引擎處理指令碼評估的方式各不相同時,這會有什麼變化。

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

您為了評估指令碼而分派的工作數量,通常與網頁上的 <script> 元素數量直接相關。每個 <script> 元素都會啟動一項工作,評估要求的指令碼,以便剖析、編譯及執行。如果是採用 Chromium 的瀏覽器、Safari 和 Firefox,就會發生這種情形。

這一點的重要性在於,假設您使用資訊整合工具來管理實際工作環境的指令碼,並將網頁需要的所有資訊彙整成單一指令碼,如果是您的網站,您應該會調度單一工作來評估該指令碼。這是不好的事情嗎?不一定,除非該指令碼龐大

如要中斷指令碼評估工作,請避免載入大量的 JavaScript 區塊,並使用額外的 <script> 元素載入更多獨立、較小的指令碼。

雖然您應盡量在網頁載入期間盡量減少載入 JavaScript,但分割指令碼可以確保,不像單一大型工作會封鎖主執行緒,而是給您更多的較小工作完全不會封鎖主執行緒,或者至少比您開始載入的方式更少。

多項工作涉及指令碼評估作業,在 Chrome 開發人員工具的效能分析器中以圖表呈現。由於載入多個較小的指令碼而非較大型的指令碼,因此工作較不可能成為長時間工作,讓主執行緒更快回應使用者輸入內容。
如果網頁的 HTML 中含有多個 <script> 元素,必須執行多項工作來評估指令碼。比起傳送大型指令碼組給使用者,這較可能封鎖主執行緒。

您可以將用於指令碼評估的工作拆分為類似互動期間執行的事件回呼。然而,在指令碼評估過程中,收益機制會將您載入的 JavaScript 分成多個較小的指令碼,而不是像封鎖主執行緒機率更高的指令碼。

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

您現在可以使用 <script> 元素上的 type=module 屬性,在瀏覽器中以原生方式載入 ES 模組。這種載入指令碼的方法對開發人員帶來一些好處,例如無需轉換程式碼就能用於實際工作環境,尤其是搭配匯入地圖使用時。不過,以這種方式載入指令碼,就會排定因瀏覽器而異的工作。

以 Chromium 為基礎的瀏覽器

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

模組編譯作業可在 Chrome 開發人員工具中以視覺化方式呈現多項工作。
以 Chromium 為基礎的瀏覽器中的模組載入行為。每個模組指令碼都會產生一個編譯模組呼叫,以便在評估前編譯其內容。

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

在 Chrome 開發人員工具的效能面板中,及時評估模組。
模組中的程式碼執行時,系統會及時評估該模組。

在 Chrome 和相關瀏覽器中,至少會產生 ES 模組的編譯步驟。這在管理長時間工作方面明顯勝利然而,最終的模組評估結果仍意味著您必須產生一些不可避免的費用。雖然無論使用何種瀏覽器,ES 模組仍應盡量減少 JavaScript 的發布方式,但有以下優點:

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

Safari 和 Firefox

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

透過動態 import() 載入的指令碼

動態 import() 是載入指令碼的另一種方法。動態 import() 呼叫不必置於 ES 模組頂端,而動態 import() 呼叫可以隨需載入 JavaScript 區塊。import這種技術稱為程式碼分割

做為改善 INP 而言,動態 import() 有兩項優點:

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

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

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

網路工作站是特殊的 JavaScript 使用案例。Web Worker 會在主執行緒上註冊,而工作站中的程式碼會在自己的執行緒上執行。這對註冊網路工作站的程式碼在主執行緒上執行時非常實用,但網路工作站內的程式碼並非如此。這可以減少主執行緒的壅塞情形,並有助於讓主執行緒回應使用者互動。

除了減少主執行緒作業外,網路工作站「本身」也能透過支援模組工作站的瀏覽器中的 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,也可以使用 modulepreload 資源提示來規避巢狀模組呼叫,這樣就能預先預先載入 ES 模組來避免網路要求鏈。

結論

對瀏覽器指令碼的評估成效有利無害。方法取決於網站的規定和限制。然而,藉由分割指令碼,您等於將指令碼評估工作分散在許多較小型的工作上,因此主執行緒能夠更有效率地處理使用者互動,而非封鎖主執行緒。

總結來說,您可以採取下列做法來分離大型指令碼評估工作:

  • 使用沒有 type=module 屬性的 <script> 元素載入指令碼時,請避免載入非常大的指令碼,因為這樣會啟動會封鎖主執行緒的資源密集型指令碼評估工作。將指令碼分散在更多 <script> 元素上,以分段這項作業。
  • 如果您使用 type=module 屬性在瀏覽器中原生載入 ES 模組,系統會針對每個個別模組指令碼啟動個別評估工作。
  • 使用動態 import() 呼叫縮減初始套件的大小。這也適用於套裝組合,因為套裝組合工具會將每個動態匯入模組視為「分割點」。為每個動態匯入模組產生獨立的指令碼。
  • 請務必權衡利弊,例如壓縮效率和快取撤銷。較大型的指令碼雖然較容易壓縮,但就比較可能涉及高成本的指令碼評估工作,所需工作更少,甚至會導致瀏覽器快取失效,導致整體快取效率降低。
  • 如果以原生方式使用 ES 模組而不整合,請使用 modulepreload 資源提示,最佳化這類模組在啟動期間的載入方式。
  • 一如既往,盡量減少 JavaScript。

這麼做有助於取得平衡,但只要使用動態 import() 分割指令碼並減少初始酬載,就能提升啟動效能,並妥善因應重要啟動期間的使用者互動。這有助於提高 INP 指標的分數,進而提供更優質的使用者體驗。

主頁橫幅由 Markus Spiske 提供,由 Unsplash 提供。