發布日期:2024 年 3 月 6 日
如果長時間工作讓主執行緒保持忙碌,導致無法執行其他重要工作 (例如回應使用者輸入內容),網頁就會變得遲緩且無回應。因此,即使是內建的表單控制項,在使用者眼中也可能會看起來有問題 (就像網頁凍結一樣),更不用說更複雜的自訂元件。
scheduler.yield()
是一種讓出主執行緒的方法,可讓瀏覽器執行任何待處理的高優先順序工作,然後從中斷處繼續執行。這麼做可讓網頁更有回應,進而改善與下一個顯示的內容互動 (INP)。
scheduler.yield
提供人體工學 API,可確切執行其所呼叫的函式:在 await scheduler.yield()
運算式中暫停執行,並將結果傳回至主執行緒,以便分割工作。函式的其餘部分執行作業 (稱為函式的繼續執行) 會排程在新事件迴圈工作中執行。
async function respondToUserClick() {
giveImmediateFeedback();
await scheduler.yield(); // Yield to the main thread.
slowerComputation();
}
scheduler.yield
的具體優點在於,系統會在排程產生後的後續動作中,在執行任何其他網頁排入佇列的類似工作之前,執行這些工作。優先處理續行工作,而非啟動新工作。
setTimeout
或 scheduler.postTask
等函式也可以用來分割工作,但這些後續動作通常會在任何已排入佇列的新工作完成後執行,因此在將工作交給主執行緒和完成工作之間,可能會出現長時間延遲的情況。
讓出後的優先順序續行
scheduler.yield
是 優先排程工作 API 的一部分。做為網頁開發人員,我們通常不會討論事件迴圈以明確優先順序執行工作任務的順序,但相對優先順序一向存在,例如在任何排入佇列的 setTimeout
回呼之後執行的 requestIdleCallback
回呼,或是在以 setTimeout(callback, 0)
排入佇列的工作任務之前執行的觸發輸入事件事件監聽器。
優先排程工作功能可讓您更輕鬆地判斷哪項工作會先執行,並在必要時調整優先順序來變更執行順序。
如前所述,在使用 scheduler.yield()
產生後,繼續執行函式的優先順序會高於啟動其他工作。指導概念是,應先執行工作續行作業,再繼續執行其他工作。如果工作是良好的程式碼,且會定期產生,讓瀏覽器可以執行其他重要作業 (例如回應使用者輸入內容),則不應因產生而受到懲罰,例如在其他類似工作之後獲得優先順序。
以下提供範例:兩個函式,排入佇列,使用 setTimeout
在不同的工作中執行。
setTimeout(myJob);
setTimeout(someoneElsesJob);
在本例中,兩個 setTimeout
呼叫彼此相鄰,但在實際網頁中,這兩個呼叫可能會在完全不同的位置呼叫,例如第一方指令碼和第三方指令碼各自設定要執行的工作,或是在架構排程器深層觸發的兩個不同元件工作。
以下是 DevTools 中可能的運作方式:
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
已排定執行後執行,因此執行作業會如下所示:
我們已使用 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();
}
執行作業現在會如下所示:
瀏覽器仍有機會回應,但現在 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-polyfill
是 scheduler.postTask
和 scheduler.yield
的小型 polyfill,在內部使用多種方法組合,模擬其他瀏覽器中許多排程 API 的強大功能 (但不支援 scheduler.yield()
優先順序繼承)。
如果您想避免使用 polyfill,可以使用 setTimeout()
產生項目,並接受優先順序續行作業的損失,甚至在無法接受的情況下,不產生項目。如需更多資訊,請參閱「改善長時間工作」中的scheduler.yield()
說明文件。
如果您要偵測 scheduler.yield()
功能並自行新增備用功能,也可以使用 wicg-task-scheduling
類型來取得類型檢查和 IDE 支援。
瞭解詳情
如要進一步瞭解 API 及其與工作優先順序和 scheduler.postTask()
的互動方式,請參閱 MDN 上的 scheduler.yield()
和「優先工作排程安排」說明文件。
如要進一步瞭解長時間工作、這些工作對使用者體驗的影響,以及如何處理這些工作,請參閱「最佳化長時間工作」一文。