針對長時間工作進行最佳化調整

一般來說,可用來加快 JavaScript 應用程式速度的建議通常包括「不要封鎖主執行緒」和「分割長時間工作」。本頁會詳細介紹這些建議的意義,並說明在 JavaScript 中最佳化工作的重要性。

什麼是工作?

工作是指瀏覽器的任何獨立工作。這包括轉譯、剖析 HTML 和 CSS、執行您編寫的 JavaScript 程式碼,以及您可能無法直接控管的其他項目。網頁的 JavaScript 是瀏覽器工作的主要來源。

Chrome 開發人員工具效能剖析器中的工作螢幕截圖。工作位於堆疊頂端,其中包含點擊事件處理常式、函式呼叫,以及其下方的更多項目。這項工作也會在右側提供一些轉譯工作。
click 事件處理常式啟動的工作,會顯示在 Chrome 開發人員工具的效能分析器中。

任務會對效能產生不同影響。舉例來說,瀏覽器在啟動期間下載 JavaScript 檔案時,會將工作排入佇列來剖析及編譯 JavaScript,以利執行。在頁面生命週期的後續階段,其他工作會在 JavaScript 執行工作時開始執行,例如透過事件處理常式、JavaScript 型動畫,以及背景活動 (例如數據分析收集) 啟動互動。除網路工作站和類似 API 以外,所有功能都會在主執行緒上發生。

什麼是主執行緒?

主執行緒是在瀏覽器中執行大部分工作的地方,幾乎所有您編寫的 JavaScript 都會執行。

主執行緒一次只能處理一項工作。凡是執行時間超過 50 毫秒的工作,都會計為「長時間工作」。如果使用者在長時間工作或轉譯更新期間嘗試與網頁互動,瀏覽器必須等候處理該互動,進而造成延遲。

Chrome 開發人員工具效能分析器中的長時間工作。工作的封鎖部分 (超過 50 毫秒) 會以紅色對角線標示。
Chrome 效能分析器中顯示的長時間工作。長時間工作會以紅色三角形標示,工作區塊的阻斷部分已填滿為對角線的紅色條紋。

為避免這種情況,請將每項長時間工作劃分成較小的工作,每個工作執行時間都會比較少。這稱為「拆分長時間的工作。

單一長時間工作與拆分為較短的工作。長時間的工作為一個大矩形,而區塊化工作為五個小方塊,其長度會延長到長時間工作的時間長度。
以視覺化方式呈現一項長時間工作,以及將同一工作分成五項較短的工作。

將工作串連起來可讓瀏覽器有更多機會回應優先順序較高的工作,包括使用者互動。這樣可以加快互動速度,使用者可能在等待長時間工作完成時,可能會發現有延遲的情況。

分散工作有助於使用者進行互動。頂端的長時間工作會封鎖事件處理常式執行,直到工作完成為止。底部的區塊工作可讓事件處理常式比其他工作更快執行。
如果工作時間過長,瀏覽器會暫時無法回應互動。分散工作可以加快這些互動的速度。

工作管理策略

JavaScript 會將各個函式視為單一工作,因為此函式會使用工作執行的執行到完成模型。也就是說,如果函式呼叫了其他多個函式 (如以下範例),就必須執行到所有已呼叫的函式都執行完畢,導致瀏覽器速度變慢:

function saveSettings () { //This is a long task.
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
Chrome 效能分析器中顯示的 saveSettings 功能。雖然頂層函式呼叫了另外五個函式,但所有工作都是在封鎖主執行緒的一項長時間工作中進行。
一個呼叫五個函式的函式 saveSettings()。該工作屬於長時間的單體式工作的一部分。

如果程式碼包含會呼叫多個方法的函式,請將其分成多個函式。這不僅讓瀏覽器有更多回應互動的機會,也能讓程式碼更容易讀取、維護及撰寫測試。以下各節將介紹一些如何分離長函式的策略,並排定函式的優先順序。

手動延遲執行程式碼

您可以將相關函式傳遞至 setTimeout(),藉此延後部分工作的執行作業。即使您指定 0 的逾時時間,一樣能正常運作。

function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Defer work that isn't user-visible to a separate task:
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

這最適合用於需要依序執行的一系列函式。如果程式碼的分類方式不同,就需要採用不同方法。下一個範例是使用迴圈處理大量資料的函式。資料集越大,這項作業所需時間就越長,而且迴圈中未必適合放置 setTimeout()

function processData () {
  for (const item of largeDataArray) {
    // Process the individual item here.
  }
}

幸運的是,其他 API 可讓您將程式碼執行延後到較新的工作上。建議您使用 postMessage() 以加快逾時

您也可以使用 requestIdleCallback() 來分解工作,但它會按照最低優先順序安排工作,且只會在瀏覽器閒置期間執行;也就是說,如果主執行緒特別忙碌,可能就無法執行以 requestIdleCallback() 排程的工作。

使用 async/await 建立收益點

為了確保使用者會在優先順序較低的工作之前發生重要工作,請短暫中斷工作佇列,讓瀏覽器有機會執行更重要的工作,藉此連線至主執行緒

最簡單的做法是加入透過呼叫 setTimeout() 解析的 Promise

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

saveSettings() 函式中,如果您在每個步驟後都為 yieldToMain() 函式 await,即可在每個步驟後產生主執行緒。這可將長時間工作有效地分解為多項工作:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread:
    await yieldToMain();
  }
}

重點:您不必在每次呼叫函式後產生結果。舉例來說,如果您執行兩個函式,且這兩個函式會對使用者介面進行重大更新,您可能不會想要在這兩個函式之間產生重大更新。如果可以的話,請讓該工作先執行,「然後」考慮在執行背景或對使用者看不到的低重要工作之間產生函式。

而 Chrome 效能分析器中的 saveSettings 功能現已產生。此工作現在分為五項不同的工作,每個工作各一項。
saveSettings() 函式現在會以個別工作的形式執行子項函式。

專屬排程器 API

目前為止提到的 API 有助於分解工作,但它們有極大的缺點:當您將程式碼延後到在後續工作中執行以產生主要執行緒時,該程式碼會新增至工作佇列的結尾。

如果您可以控制網頁上的所有程式碼,可以建立自己的排程器來排定工作的優先順序。不過,第三方指令碼不會使用您的排程器,因此您實際上無法排定工作的優先順序。只能將其細分或促成使用者互動。

瀏覽器支援

  • 94
  • 94
  • x

資料來源

排程器 API 提供 postTask() 函式,可更精細地安排工作,並可協助瀏覽器排定工作的優先順序,讓低優先順序的工作產生主要執行緒。postTask() 會使用 promise 並接受 priority 設定。

postTask() API 有三個可用的優先順序:

  • 'background',用於優先順序最低的工作。
  • 'user-visible',適用於中優先順序的工作。如果未設定 priority,這會是預設值。
  • 'user-blocking' 代表需要高優先順序執行的重要工作。

以下範例程式碼使用 postTask() API,以最高優先順序執行三項工作,其餘兩項工作的優先順序則是最低:

function saveSettings () {
  // Validate the form at high priority
  scheduler.postTask(validateForm, {priority: 'user-blocking'});

  // Show the spinner at high priority:
  scheduler.postTask(showSpinner, {priority: 'user-blocking'});

  // Update the database in the background:
  scheduler.postTask(saveToDatabase, {priority: 'background'});

  // Update the user interface at high priority:
  scheduler.postTask(updateUI, {priority: 'user-blocking'});

  // Send analytics data in the background:
  scheduler.postTask(sendAnalytics, {priority: 'background'});
};

在這裡,為工作排定優先順序,方便使用者互動等瀏覽器優先工作來運作。

Chrome 效能分析器中顯示的 SaveSettings 函式,但使用 postTask。postTask 會分割每個 函式 saveSettings 的執行作業,並排出優先順序,確保使用者互動可以順利執行,而不會遭到封鎖。
saveSettings() 執行時,函式會使用 postTask() 排定個別函式呼叫,面向使用者的重要工作會排定在高優先順序,而使用者不知道的工作則安排在背景執行。如此一來,由於工作會分解並適當排列,因此能加快使用者互動的執行速度。

您也可以對不同工作之間共用優先順序的不同 TaskController 物件執行個體化,包括視需要變更不同 TaskController 例項的優先順序。

使用即將推出的 scheduler.yield() API 搭配持續性的內建收益功能

重點提示:如需有關 scheduler.yield() 的詳細說明,請參閱來源試用 (經過結論) 以及其說明

建議您為排程器 API 新增 scheduler.yield(),這是專為在瀏覽器中產生主執行緒而設計的 API。該函式使用與本頁先前示範的 yieldToMain() 函式類似:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread with the scheduler
    // API's own yielding mechanism:
    await scheduler.yield();
  }
}

此程式碼大致上很熟悉,但並不是使用 yieldToMain(),而是使用 await scheduler.yield()

三個圖表顯示工作都不會產生及產生,且產生及接續。無法產生成效,會進行長時間的任務產生產生的工作可以讓更多工作更短,但可能會因為其他不相關的工作而中斷。隨著產生和持續,會保留較短的工作執行順序。
使用 scheduler.yield() 時,工作執行作業會從上次中斷的地方繼續,即使在產生點之後也一樣。

scheduler.yield() 的好處是連續;也就是說,如果您在一組任務中途產生,其他排定的工作會在產生點之後,按照相同的順序繼續。這可防止第三方指令碼控製程式碼執行的順序。

此外,由於 user-blocking 的優先順序較高,將 scheduler.postTask()priority: 'user-blocking' 搭配使用也非常有可能延續。因此,在 scheduler.yield() 進一步開放使用前,您可以做為替代方案。

使用 setTimeout() (或 scheduler.postTask() 搭配 priority: 'user-visible' 或不明確 priority) 會將工作安排在佇列的背面,讓其他待處理工作在接續前執行。

使用 isInputPending() 輸入輸入收益

瀏覽器支援

  • 87
  • 87
  • x
  • x

透過 isInputPending() API,您可以檢查使用者是否已嘗試與網頁互動,並且只在輸入內容待處理時才產生。

這樣在沒有待處理的輸入的情況下,JavaScript 會繼續運作,而非產生並結束於工作佇列的後端。這可能會導致效能大幅提升。如要出貨的意圖所述,這樣做可以針對可能不會傳回主執行緒的網站,提供出色的效能。

然而,自該 API 推出後,我們對收益的理解也有所提升,尤其是在 INP 引入之後。我們不再建議使用這個 API,而無論輸入內容是否處於待處理狀態,都建議產生資料。這項建議變更是基於下列原因造成:

  • 在某些情況下,如果使用者已互動,API 可能會錯誤傳回 false
  • 輸入內容通常不是唯一能產生工作的情況。動畫和其他一般使用者介面更新對於提供回應式網頁也同樣重要。
  • 現已推出更全面的 API (例如 scheduler.postTask()scheduler.yield()) 以解決相關問題。

結論

管理工作很有挑戰性,但這樣做可讓您的網頁更快回應使用者互動。您可以根據自身用途,運用多種技術管理及排定工作的優先順序。再次重申,管理工作時,建議您考量以下主要問題:

  • 傳回至主執行緒,供使用者執行的重要工作。
  • 建議你透過 scheduler.yield() 進行實驗。
  • 使用 postTask() 排定工作的優先順序。
  • 最後,盡可能減少函式中的工作。

透過這些工具,您應該可以建構應用程式中的工作,以優先滿足使用者需求,同時確保較少重要工作完成。藉此提升使用者體驗,使其更敏捷且更易於使用。

特別感謝 Philip Walton 對這份文件進行技術審查。

縮圖圖片來源為 Unsplash (由 Amirali Mirhashemian 提供)。