載入指令碼時,瀏覽器需要時間評估指令碼,然後才能執行,這可能會導致長時間工作。瞭解指令碼評估的運作方式,以及如何避免指令碼評估在載入網頁期間造成長時間任務。
談到如何改善Interaction to Next Paint (INP) 指標,您會發現大部分建議都是要改善互動本身。舉例來說,在最佳化長時間執行的工作指南中,我們討論了使用 setTimeout 等方式產生結果的技術。這些技巧很有幫助,因為可避免長時間任務,讓主執行緒有喘息空間,進而讓互動和其他活動有更多機會提早執行,不必等待單一長時間任務完成。
不過,載入指令碼本身也會產生長時間執行的工作,這些工作可能會干擾使用者互動,並影響網頁載入期間的 INP。本指南將探討瀏覽器如何處理指令碼評估啟動的工作,並瞭解如何分解指令碼評估工作,讓主要執行緒在頁面載入時,能更快速回應使用者輸入內容。
什麼是指令碼評估?
如果您分析的應用程式會傳送大量 JavaScript,可能會看到標示為「評估指令碼」的長時間工作。
在瀏覽器中執行 JavaScript 時,必須先評估指令碼,因為 JavaScript 是在執行前即時編譯。評估指令碼時,系統會先剖析指令碼是否有錯誤。如果剖析器未發現錯誤,系統就會將指令碼編譯為位元組碼,然後繼續執行。
雖然必要,但指令碼評估可能會造成問題,因為使用者可能會在網頁首次算繪後不久,就嘗試與網頁互動。不過,網頁算繪完成並不代表載入完成。網頁忙於評估指令碼時,載入期間發生的互動可能會延遲。雖然無法保證此時可以進行互動 (因為負責互動的指令碼可能尚未載入),但可能會有已準備就緒的 JavaScript 互動,或互動完全不依賴 JavaScript。
指令碼與評估指令碼的任務之間的關係
負責指令碼評估的工作啟動方式,取決於您載入的指令碼是否透過一般 <script> 元素載入,或是指令碼是否為透過 type=module 載入的模組。由於瀏覽器處理事項的方式往往不同,因此本文會說明主要瀏覽器引擎如何處理指令碼評估,以及這些引擎的指令碼評估行為差異。
使用 <script> 元素載入的指令碼
一般來說,指派的指令碼評估工作數量與網頁上的 <script> 元素數量直接相關。每個 <script> 元素都會啟動工作,評估所要求的指令碼,以便進行剖析、編譯及執行。以 Chromium 架構為基礎的瀏覽器、Safari 和 Firefox 都是如此。
這一點的重要性在於,假設您使用打包工具管理製作指令碼,並已將其設定為將網頁執行所需的所有內容打包成單一指令碼。如果您的網站屬於這種情況,系統會指派單一工作來評估該指令碼。這樣不好嗎?不一定,除非該指令碼非常龐大。
您可以避免載入大量 JavaScript,並使用額外的 <script> 元素載入更多個別的小型指令碼,藉此分解指令碼評估工作。
雖然您應盡量在載入網頁期間載入最少的 JavaScript,但分割指令碼可確保您擁有大量不會封鎖主執行緒的小型任務,而不是可能封鎖主執行緒的大型任務,或至少比您開始時的封鎖程度更低。
<script> 元素,因此系統會產生多項工作來評估指令碼。相較於傳送一個大型指令碼套件給使用者,這種做法較不容易封鎖主執行緒。您可以將指令碼評估工作拆分,這與在互動期間執行的事件回呼中產生結果有些類似。不過,透過指令碼評估,產生機制會將載入的 JavaScript 分成多個較小的指令碼,而不是分成較少但較大的指令碼,這樣比較不會封鎖主要執行緒。
使用 <script> 元素和 type=module 屬性載入的指令碼
現在只要在 <script> 元素上使用 type=module 屬性,即可在瀏覽器中以原生方式載入 ES 模組。這種指令碼載入方式可為開發人員帶來一些好處,例如不必轉換程式碼以供正式環境使用 (特別是與匯入對應搭配使用時)。不過,以這種方式載入指令碼會排定工作,但不同瀏覽器排定的工作有所不同。
以 Chromium 為基礎的瀏覽器
在 Chrome 等瀏覽器 (或衍生自 Chrome 的瀏覽器) 中,使用 type=module 屬性載入 ES 模組時,產生的工作類型與未使用 type=module 時通常不同。舉例來說,每個模組指令碼都會執行一項工作,其中包含標示為「編譯模組」的活動。
模組編譯完成後,後續在模組中執行的任何程式碼都會啟動標示為「評估模組」的活動。
至少在 Chrome 和相關瀏覽器中,使用 ES 模組時,編譯步驟會中斷。就管理長時間工作而言,這顯然是個好方法;不過,這樣做仍會產生一些無法避免的費用。雖然您應盡量減少傳送的 JavaScript,但使用 ES 模組 (無論瀏覽器為何) 可帶來下列好處:
- 所有模組程式碼都會以嚴格模式自動執行,讓 JavaScript 引擎進行潛在最佳化,否則無法在非嚴格環境中進行。
- 使用
type=module載入的指令碼預設會視為延遲。如要變更這項行為,可以對使用type=module載入的指令碼使用async屬性。
Safari 和 Firefox
在 Safari 和 Firefox 中載入模組時,系統會分別評估每個模組。也就是說,理論上您可以將只包含 static import 陳述式的單一頂層模組載入其他模組,而載入的每個模組都會產生個別的網路要求和工作,以評估該模組。
使用動態 import() 載入的指令碼
動態 import() 是載入指令碼的另一種方法。與必須位於 ES 模組頂端的靜態 import 陳述式不同,動態 import() 呼叫可以出現在指令碼中的任何位置,以便視需要載入 JavaScript 區塊。這項技術稱為「程式碼分割」。
動態 import() 在改善 INP 方面有兩項優點:
- 延後載入的模組可減少啟動期間載入的 JavaScript 數量,進而減少主執行緒爭用情形。這樣一來,主要執行緒就能更快速地回應使用者互動。
- 動態
import()呼叫時,每個呼叫都會將各個模組的編譯和評估作業有效區隔到各自的工作。當然,如果動態import()載入的模組非常大,就會啟動相當大的指令碼評估工作,如果互動與動態import()呼叫同時發生,可能會干擾主執行緒回應使用者輸入內容的能力。因此,盡可能減少載入的 JavaScript 仍非常重要。
動態 import() 呼叫在所有主要瀏覽器引擎中的行為都類似:產生的指令碼評估工作數量,與動態匯入的模組數量相同。
在網頁背景工作執行緒中載入的指令碼
網頁工作站是 JavaScript 的特殊用途。網頁工作人員會在主執行緒上註冊,工作人員內的程式碼隨後會在自己的執行緒上執行。這項功能非常實用,因為註冊網頁背景工作人員的程式碼會在主執行緒上執行,但網頁背景工作人員中的程式碼則不會。這可減少主要執行緒的壅塞情況,並協助主要執行緒對使用者互動保持更高的回應速度。
除了減少主執行緒工作,網頁工作人員本身也可以載入外部指令碼,以便在工作人員環境中使用,方法是透過 importScripts 或支援模組工作人員的瀏覽器中的靜態 import 陳述式。因此,網頁工作站要求的任何指令碼都會在主執行緒外進行評估。
取捨和考量
將指令碼分成較小的檔案,有助於限制長時間工作,而不是載入較少但較大的檔案。不過,決定如何分割指令碼時,請務必考量下列事項。
壓縮效率
壓縮是分割指令碼時的考量因素。如果指令碼較小,壓縮效率會略為降低。壓縮功能對較大的指令碼更有幫助。提高壓縮效率有助於盡量縮短指令碼的載入時間,但為了確保您將指令碼分成足夠小的區塊,以便在啟動期間提升互動性,這需要一些平衡。
對於網站所依附的指令碼,您可以透過 Bundler 管理輸出大小:
- 就 webpack 而言,其
SplitChunksPlugin外掛程式可提供協助。如要瞭解可設定的選項,協助管理素材資源大小,請參閱SplitChunksPlugin說明文件。 - 如果是 Rollup 和 esbuild 等其他打包工具,您可以在程式碼中使用動態
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 傳送另一個網路要求,然後 b.js 會再為 c.js 傳送要求。避免這個問題的方法之一是使用打包工具,但請務必設定打包工具,將指令碼拆開,分散指令碼評估工作。
如果不想使用 Bundler,可以改用modulepreload 資源提示,預先載入 ES 模組,避免網路要求鏈結,解決巢狀模組呼叫問題。
結論
在瀏覽器中評估指令碼的過程相當複雜,具體做法取決於網站的需求和限制。不過,藉由分割指令碼,您可以將指令碼評估工作分散到多個較小的工作,因此主執行緒能夠更有效率地處理使用者互動,而不是阻斷主執行緒。
總而言之,您可以採取下列措施,將大型指令碼評估工作拆分成多個部分:
- 使用
<script>元素載入指令碼時,請避免載入過大的指令碼,否則會啟動耗用大量資源的指令碼評估工作,導致主執行緒遭到封鎖。type=module將指令碼分散到更多<script>元素中,以分解這項工作。 - 使用
type=module屬性在本機瀏覽器中載入 ES 模組,會針對每個獨立模組指令碼啟動個別評估工作。 - 使用動態
import()呼叫縮減初始套件的大小。這也適用於打包工具,因為打包工具會將每個動態匯入的模組視為「分割點」,因此會為每個動態匯入的模組產生個別的指令碼。 - 請務必權衡壓縮效率和快取失效等取捨因素。較大的指令碼壓縮效果較好,但可能導致較少工作涉及成本較高的指令碼評估工作,並造成瀏覽器快取失效,進而降低整體快取效率。
- 如果使用原生 ES 模組而不進行組合,請使用
modulepreload資源提示,在啟動期間最佳化模組載入作業。 - 請盡量減少傳送的 JavaScript。
這當然需要權衡取捨,但只要使用動態 import() 分割指令碼並減少初始有效負載,就能提升啟動效能,並在啟動期間配合使用者互動。這有助於提高 INP 指標分數,進而提供更優質的使用者體驗。