打造可快速回應使用者意見的網站,是網路效能最棘手的問題之一,Chrome 團隊一直在努力協助網頁開發人員。今年稍早,我們宣布「與下一個顯示的內容互動」指標將從實驗狀態升級為待定狀態。如今,INP 已準備好在 2024 年 3 月取代 First Input Delay (FID),成為 Core Web Vitals 之一。
為了持續提供新的 API,協助網頁開發人員讓網站盡可能快速,Chrome 團隊目前正在 Chrome 115 版中執行 scheduler.yield
的來源試用版。scheduler.yield
是建議的排程器 API 新增功能,相較於過去仰賴的方法,可以更輕鬆有效地將控制權移回主執行緒。
在產生時
JavaScript 會使用「執行到完成」模型來處理工作。也就是說,當工作在主執行緒上執行時,該工作會在必要時執行,直到完成為止。工作完成後,系統會將控制權交出至主執行緒,讓主執行緒處理佇列中的下一個工作。
除了工作永遠無法完成的極端情況 (例如無限迴圈) 之外,JavaScript 的工作排程邏輯不可避免地會產生交出動作。而是「何時」發生,但以後再來會比「之後」來得好。如果工作耗時過長 (具體來說,超過 50 毫秒),系統會將其視為長時間工作。
長時間的工作會導致網頁回應速度變慢,因為這會延遲瀏覽器回應使用者輸入內容的速度。長時間工作發生的次數越多,執行時間越長,使用者就越有可能認為網頁速度緩慢,甚至認為網頁完全無法使用。
不過,即使程式碼在瀏覽器中啟動工作,也不代表您必須等到該工作完成,才能將控制權交還給主執行緒。您可以透過在工作中明確地交出,改善對網頁上使用者輸入內容的回應速度,這會將工作分割成可在下次可用機會完成的工作。這樣一來,其他工作就能在主執行緒上執行,不必等待長時間的工作完成。
明確執行 yield 時,您會告訴瀏覽器:「我知道我要執行的工作可能需要一段時間,但我不希望您在回應使用者輸入內容或其他可能重要的工作之前,就必須執行所有這項工作。」這項工具是開發人員工具箱中的重要工具,可大幅改善使用者體驗。
目前收益策略的問題
產生 setTimeout
的常見方法,會使用逾時值為 0
。這項功能之所以有效,是因為傳遞至 setTimeout
的回呼會將剩餘的工作移至另外排入佇列,供後續執行的工作使用。您不必等待瀏覽器自行產生版本,而是說:「我們把這龐大的工作切割為更小的點吧」。
但是,使用 setTimeout
產生可能會產生不理想的副作用,也就是產生收益點「之後」發生的工作會回到工作佇列的背面。由使用者互動排程的工作仍會如預期排入佇列,但您在明確讓步後想要執行的其他工作,可能會因排在前面的其他競爭來源而進一步延遲。
如要瞭解實際運作方式,請觀看此 Glitch 示範,或在下方嵌入版本試用看看。這個示範包含幾個可點選的按鈕,以及下方用於記錄工作執行時間的方塊。進入該網頁後,請執行下列動作:
- 按一下頂端的「定期執行工作」按鈕,系統就會排定每隔一段時間執行一次的阻斷工作。點選這個按鈕後,工作記錄會填入多則訊息,內容為「Ran blocking task with
setInterval
」。 - 接著,按一下「Run loop, yielding with
setTimeout
on each iteration」標示的按鈕。
您會發現,在示範的底部方塊中會顯示類似以下的內容:
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
。 - 啟用「實驗性網頁平台功能」實驗。完成後,你可能需要重新啟動 Chrome。
- 前往示範頁面,或使用清單下方的內嵌版本。
- 按一下頂端的「定期執行工作」按鈕。
- 最後,請按一下「Run loop, yielding with
scheduler.yield
on each iteration」標示的按鈕。
頁面底部方塊中的輸出內容會如下所示:
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 平台功能」部分的下拉式選單中選取「啟用」。這樣一來,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 的品質,並提供意見,協助我們改善這項功能。
主頁橫幅圖片取自 Unsplash,圖片作者:Jonathan Allison。