使用 scheduler.yield() 細分長時間的工作

Brendan Kenny
Brendan Kenny

發布日期:2025 年 3 月 6 日

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • Safari: not supported.

Source

如果長時間執行的工作持續占用主執行緒,導致主執行緒無法執行其他重要工作 (例如回應使用者輸入內容),網頁就會顯得緩慢且沒有回應。因此,即使是內建表單控制項,使用者也可能會覺得無法正常運作 (像是網頁凍結),更別說是較複雜的自訂元件。

scheduler.yield() 可讓您將控制權交給主執行緒,讓瀏覽器執行任何待處理的高優先順序工作,然後從中斷處繼續執行。這有助於提升網頁的回應速度,進而改善與下一個顯示的內容互動 (INP)

scheduler.yield 提供符合人體工學的 API,可執行以下作業:在 await scheduler.yield() 運算式暫停呼叫函式,並產生至主執行緒,中斷工作。函式其餘部分的執行作業 (稱為函式的續集) 會排定在新事件迴圈工作執行。

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

scheduler.yield 的具體優點是,在產生後繼續執行的作業會排定在網頁佇列中的任何其他類似工作之前執行。系統會優先繼續執行工作,而非開始新工作。

setTimeoutscheduler.postTask 等函式也可用於中斷工作,但這些續傳作業通常會在所有已排入佇列的新工作之後執行,可能會導致工作產生延遲,因為這些工作會讓出主執行緒,並等待完成工作。

產生後優先執行的續傳作業

scheduler.yield優先工作排程 API 的一部分。身為網頁開發人員,我們通常不會根據明確的優先順序,討論事件迴圈執行工作的順序,但相對優先順序一向存在,例如在任何已排入佇列的 setTimeout 回呼之後執行的 requestIdleCallback 回呼,或通常在以 setTimeout(callback, 0) 排入佇列的工作之前執行的觸發輸入事件監聽器。

優先處理工作排程只是讓這項功能更加明確,方便您判斷哪些工作會先執行,並視需要調整優先順序來變更執行順序。

如前所述,在以 scheduler.yield() 產生值後繼續執行的函式,其優先順序會高於啟動其他工作。指導概念是先執行工作續傳,再繼續執行其他工作。如果工作是運作正常的程式碼,會定期產生結果,讓瀏覽器可以執行其他重要事項 (例如回應使用者輸入內容),就不應因產生結果而受到懲罰,在其他類似工作之後才獲得優先處理。

以下是範例:兩個函式已排入佇列,準備使用 setTimeout 在不同工作中執行。

setTimeout(myJob);
setTimeout(someoneElsesJob);

在這個情況下,兩個 setTimeout 呼叫會緊鄰彼此,但在實際網頁中,這兩個呼叫可能會在完全不同的位置呼叫,例如第一方指令碼和第三方指令碼會獨立設定要執行的工作,或是來自不同元件的兩個工作會在架構的排程器中深處觸發。

在開發人員工具中,這項工作可能如下所示:

Chrome 開發人員工具的「效能」面板中顯示兩項工作。兩者都顯示為長時間工作,其中函式「myJob」佔用第一項工作的整個執行時間,而「someoneElsesJob」則佔用第二項工作的整個執行時間。

myJob 會標示為長時間工作,在執行期間會阻斷瀏覽器執行其他作業。假設這是來自第一方指令碼,我們可以將其拆開:

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

由於 myJobPart2 已排定在 myJob 內與 setTimeout 一併執行,但該排程晚於 someoneElsesJob 的排程,因此執行方式如下:

Chrome 開發人員工具效能面板中顯示三項工作。第一個是執行函式「myJobPart1」,第二個是執行「someoneElsesJob」的長時間工作,最後第三個工作是執行「myJobPart2」。

我們已使用 setTimeout 將工作分成兩部分,因此瀏覽器可以在 myJob 執行期間保持回應,但現在 myJob 的第二部分只會在 someoneElsesJob 完成後執行。

有時這樣做或許沒問題,但通常不是最佳做法。myJob 是為了確保網頁能持續回應使用者輸入內容而讓步給主執行緒,並非完全放棄主執行緒。如果 someoneElsesJob 執行速度特別慢,或除了 someoneElsesJob 之外還有許多其他工作已排定,可能要過很久才會執行 myJob 的後半部分。開發人員在將 setTimeout 新增至 myJob 時,可能並非有意為之。

輸入 scheduler.yield(),將叫用該函式的任何函式續傳作業,放入比啟動任何其他類似工作略高的優先順序佇列。如果 myJob 變更為使用此項目:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

現在的執行作業如下所示:

Chrome 開發人員工具效能面板中顯示兩項工作。兩者都顯示為長時間工作,其中函式「myJob」佔用第一項工作的整個執行時間,而「someoneElsesJob」則佔用第二項工作的整個執行時間。

瀏覽器仍有機會做出回應,但現在系統會優先繼續執行 myJob 工作,而不是啟動新工作 someoneElsesJob,因此 myJob 會在 someoneElsesJob 開始前完成。這與將控制權交還給主執行緒以維持回應性的預期更接近,而不是完全放棄主執行緒。

優先順序繼承

scheduler.yield() 是較大型的「優先工作排程」API 的一部分,可與 scheduler.postTask() 中提供的明確優先順序妥善搭配使用。如果沒有明確設定優先順序,scheduler.postTask() 回呼中的 scheduler.yield() 基本上會與上一個範例相同。

不過,如果設定了優先順序,例如使用低 'background' 優先順序:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

系統會排定優先順序高於其他 'background' 工作的續傳作業,因此您會先取得預期的優先續傳作業,再取得任何待處理的 'background' 工作,但優先順序仍低於其他預設或高優先順序工作,且仍屬於 'background' 工作。

也就是說,如果您使用 'background' scheduler.postTask() (或 requestIdleCallback) 排定低優先順序工作,scheduler.yield() 之後的續傳也會等待,直到大部分其他工作完成且主執行緒閒置為止,這正是您希望低優先順序工作產生結果的方式。

如何使用 API

目前 scheduler.yield() 僅適用於以 Chromium 為基礎的瀏覽器,因此如要使用這項功能,您需要進行功能偵測,並在其他瀏覽器中改用次要的產生方式。

scheduler-polyfillscheduler.postTaskscheduler.yield 的小型 Polyfill,會在內部使用多種方法,在其他瀏覽器中模擬許多排程 API 的功能 (但 scheduler.yield() 優先權繼承功能不支援)。

如要避免使用 Polyfill,其中一種方法是使用 setTimeout() 產生,並接受優先延續作業的損失,或甚至在不支援的瀏覽器中不產生,如果這不可接受的話。詳情請參閱「scheduler.yield()」一節。

如果您自行偵測 scheduler.yield() 功能並新增備援,也可以使用wicg-task-scheduling 型別取得型別檢查和 IDE 支援。

瞭解詳情

如要進一步瞭解 API,以及 API 如何與工作優先順序和 scheduler.postTask() 互動,請參閱 MDN 上的「scheduler.yield()」和「優先工作排程」文件。

如要進一步瞭解長時間工作、對使用者體驗的影響,以及如何處理,請參閱長時間工作最佳化