打造可快速回應使用者意見的網站,是網路效能最棘手的問題之一,Chrome 團隊一直在努力協助網頁開發人員。我們在今年宣布,「與下一個繪製內容的互動」(INP) 指標會從實驗變為待處理狀態。目前預計於 2024 年 3 月,將 First Input Delay (FID) 取代為 Core Web Vitals。
為持續提供新的 API,協助網頁開發人員盡可能提高網站速度,Chrome 團隊目前正從 Chrome 115 版開始執行 scheduler.yield
的來源試用。scheduler.yield
是建議的排程器 API 新增功能,相較於過去仰賴的方法,可以更輕鬆有效地將控制權移回主執行緒。
產生
JavaScript 會使用「執行到完成」模型來處理工作。也就是說,當工作在主執行緒上執行時,該工作會視需要持續執行完成。工作完成後,控管機制會「產生」返回主執行緒,讓主執行緒處理佇列中的下一個工作。
除了工作從未完成的極端情況 (例如無限迴圈) 以外,JavaScript 的工作排程邏輯是不可避免的面向。而是「何時」發生,但以後再來會比「之後」來得好。如果工作執行時間過長 (大於 50 毫秒),就會視為「長時間工作」。
冗長工作會讓網頁回應速度過慢,因為這類工作會延遲瀏覽器回應使用者輸入內容的能力。越是發生長時間工作,執行時間越長,使用者越有可能覺得網頁運作緩慢,或甚至覺得完全損毀。
然而,即使程式碼在瀏覽器中啟動工作,也不代表必須等到工作完成後,控制項才會傳回主執行緒。您可以在工作中明確排出工作,在下一個可用機會結束以完成工作,這樣可以改善使用者在頁面上輸入動作時的回應速度。比起等待長時間工作完成,其他工作更快在主執行緒中取得時間。
![描述分割工作如何提升輸入回應速度。在頂端,長時間的工作會阻止事件處理常式執行,直到工作完成為止。在底部,分割的工作會允許事件處理常式的執行速度比其他情況更快。](https://developer.chrome.google.cn/static/blog/introducing-scheduler-yield-origin-trial/image/a-depiction-how-breaking-1f8a83887e582.png?authuser=19&hl=zh-tw)
獲得明確結果時,系統是告知瀏覽器「嘿,我本來要做的事可能需要一點時間,我不希望在回應使用者輸入內容或其他重要工作之前,先執行所有工作」。它是開發人員工具箱中的重要工具,可有效改善使用者體驗。
目前收益策略的問題
常見的產生方法會使用逾時值為 0
的 setTimeout
。這項功能之所以有效,是因為傳遞至 setTimeout
的回呼會將剩餘的工作移至另外排入佇列,供後續執行的工作使用。與其等待瀏覽器自行產生版本,不如「讓我們把這塊龐大工作分拆成更小的點」。
但是,使用 setTimeout
產生可能會產生不想要的副作用,也就是收益點「之後」發生的工作會回到工作佇列的背面。由使用者互動排定的工作仍會如預期顯示在佇列最前方;不過,在明確得到這些工作後,如果您想執行其餘工作,最後可能會因為排隊其他競爭來源的工作而延後完成。
如要瞭解實際運作方式,請觀看此 Glitch 示範,或在下方嵌入版本試用看看。這個示範包含幾個按鈕,可供您點選,並在這些按鈕下方顯示記錄工作執行時的方塊。進入該網頁後,請執行下列動作:
- 按一下頂端標示為「定期執行任務」的按鈕,即可排定讓封鎖工作頻繁執行。您點選這個按鈕後,工作記錄會填入幾則訊息:使用
setInterval
執行封鎖工作。 - 接著,按一下標示「Run 循環 (執行迴圈,每次疊代時產生
setTimeout
)」的按鈕。
您會發現示範最下方的方塊會顯示如下的內容:
Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
此輸出內容顯示「工作佇列的結尾」產生使用 setTimeout
產生的結果執行此迴圈會處理五個項目,且每個項目都處理完畢後會產生 setTimeout
。
這說明瞭網路上常見的問題,因為指令碼 (尤其是第三方指令碼) 會註冊可在一些間隔時間運作的計時器函式,並不是特別常見的問題。工作佇列的「結尾」產生 setTimeout
所產生的行為,意味著來自其他工作來源的工作可能會排入佇列,直到產生迴圈所需的其餘工作為止。
視應用程式而定,這個結果不一定是理想的結果,但在許多情況下,這也導致開發人員不想輕易掌控主執行緒。產生收益很不錯,因為使用者互動可以更快執行,但也可以讓其他非使用者互動的工作在主執行緒上延長時間。這會是真正的問題,但 scheduler.yield
能助您一臂之力!
進入scheduler.yield
自 Chrome 115 版起,scheduler.yield
已提供實驗性網路平台功能做為旗幟。您可能會想:「為什麼 setTimeout
已經具備特殊函式,還需要其他特殊函式才能產生結果?」
值得注意的是,產生結果不是 setTimeout
的設計目標,而是安排日後的回呼在稍後執行 (即使設有逾時值 0
) 是個不錯的副作用。不過,更重要的是,使用 setTimeout
產生將剩餘工作傳送至工作佇列的「返回」。根據預設,scheduler.yield
會將剩餘工作傳送至佇列的前端。這表示您想在獲得結果後立即繼續的工作,不會被其他來源的工作所撤回 (使用者互動例外的情況除外)。
scheduler.yield
是一種函式,其會產生給主執行緒,並在呼叫時傳回 Promise
。這表示您可以在 async
函式中 await
該函式:
async function yieldy () {
// Do some work...
// ...
// Yield!
await scheduler.yield();
// Do some more work...
// ...
}
如要查看 scheduler.yield
的實際運作情形,請執行下列操作:
- 前往
chrome://flags
。 - 啟用「實驗性 Web Platform 功能」實驗。執行上述操作後,你可能必須重新啟動 Chrome。
- 請前往示範頁面或使用這份清單下方的嵌入版本。
- 按一下標有「定期執行工作」的按鈕,
- 最後,按一下標示為「Run 循環 (執行迴圈,每次疊代時產生
scheduler.yield
)」的按鈕。
頁面底部方塊中的輸出內容如下所示:
Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
與使用 setTimeout
產生的示範不同,您可以看到迴圈 (即使每次疊代後都會產生) 也不會將其餘工作傳送到佇列的背面,而是傳送至佇列的最前方。這樣做的好處是,您可以提升網站的輸入回應速度,同時確保之後要完成的工作不會延遲。
快來試試看吧!
如果你對 scheduler.yield
感興趣,而且想要試用這項功能,請從 Chrome 115 版開始採用下列兩種方法:
- 如要在本機環境中使用
scheduler.yield
進行實驗,請在 Chrome 的網址列中輸入並輸入chrome://flags
,然後在「實驗性 Web Platform 功能」部分的下拉式選單中選取「啟用」。這會讓scheduler.yield
(和其他任何實驗功能) 只能在你的 Chrome 執行個體中使用。 - 如要在可公開存取的來源中,為真正的 Chromium 使用者啟用
scheduler.yield
,您必須申請scheduler.yield
來源試用。如此一來,您就能在指定期間內對提議的功能進行安全實驗,並為 Chrome 團隊提供寶貴的深入分析,瞭解這些功能在實際環境中的使用情形。如要進一步瞭解來源試用的運作方式,請參閱這份指南。
使用 scheduler.yield
的方式 (但仍支援不支援該功能的瀏覽器) 的方式取決於您的目標。您可以使用官方的 polyfill。如果下列情況適合您使用 polyfill:
- 目前已在應用程式中使用
scheduler.postTask
來排定工作。 - 您想設定工作並產生優先順序。
- 您希望透過
scheduler.postTask
API 提供的TaskController
類別取消或重新排定工作的優先順序。
如果上述說明與您的情況不符,則可能不適合使用 polyfill。在這種情況下,您可以透過幾種方式自行推出備用方案。第一個方法會使用 scheduler.yield
(如果有的話),但如果無法使用,則會改回使用 setTimeout
:
// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
// Use scheduler.yield if it exists:
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fall back to setTimeout:
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
// Example usage:
async function doWork () {
// Do some work:
// ...
await yieldToMain();
// Do some other work:
// ...
}
這雖然有效,但或許你猜得,不支援 scheduler.yield
的瀏覽器在沒有「佇列前面」時,就會產生行為也就是說,如果您完全無法產生收益,可以嘗試使用 scheduler.yield
的其他方法 (如果有的話),但如果無效,根本無法產生:
// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
// Use scheduler.yield if it exists:
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fall back to nothing:
return;
}
// Example usage:
async function doWork () {
// Do some work:
// ...
await yieldToMain();
// Do some other work:
// ...
}
scheduler.yield
是排程器 API 的新增功能,希望能使開發人員的回應速度比目前的收益策略更加簡單。如果您覺得 scheduler.yield
這個 API 很實用,歡迎參與我們的研究,協助我們提升 API 的品質,並提供意見,協助我們改善這項功能。
主頁橫幅由 Jonathan Allison 的 Unsplash 提供。