trainr.yield 來源試用簡介

建構能快速回應使用者輸入內容的網站,一直是網頁效能方面最困難的挑戰之一,Chrome 團隊一直努力協助網頁開發人員達成這項目標。今年稍早宣布,「與下一個顯示的內容互動 (INP) 指標」將從實驗階段進入待定狀態。並預計在 2024 年 3 月取代首次輸入延遲 (FID),成為 Core Web Vitals。

為持續提供新版 API,協助網頁開發人員盡可能提升網站速度,Chrome 團隊目前正在進行來源試用,scheduler.yield將於 Chrome 115 版推出。scheduler.yield 是排程器 API 的新提案,可提供比傳統方法更簡單且更優質的方式,將控制權交還給主執行緒。

產生時

JavaScript 使用「執行至完成」模型處理工作。也就是說,當工作在主執行緒上執行時,該工作會盡可能執行,直到完成為止。工作完成後,控制權會產生回主執行緒,讓主執行緒處理佇列中的下一個工作。

除了工作永遠不會完成的極端情況 (例如無限迴圈) 之外,產生是 JavaScript 工作排程邏輯中不可避免的一環。這一定會發生,只是時間早晚的問題,而且越早越好。如果工作執行時間過長 (確切來說是超過 50 毫秒),就會視為長時間工作

長時間執行的工作會導致網頁回應速度緩慢,因為這類工作會延遲瀏覽器回應使用者輸入內容的能力。長時間執行的工作越常發生,執行時間越長,使用者就越有可能覺得網頁遲緩,甚至認為網頁完全無法運作。

不過,即使程式碼在瀏覽器中啟動工作,您也不必等到工作完成,控制權才會交還給主執行緒。您可以在工作期間明確產生結果,將工作分成多個部分,在下一個可用時機完成,藉此提升網頁對使用者輸入內容的回應速度。這樣一來,其他工作就能比等待長時間工作完成更快取得主執行緒的時間。

圖片:說明如何將工作分成多個部分,以提升輸入回應速度。在頂端,長時間任務會阻斷事件處理常式,直到任務完成為止。在底部,分塊工作允許事件處理常式比原本更早執行。
將控制權交還給主執行緒的視覺化呈現。在頂端,只有在工作執行完畢後才會產生結果,這表示工作可能需要較長時間才能完成,然後將控制權傳回主執行緒。在底部,產生作業會明確完成,將長時間任務拆分成幾個較小的工作。這樣一來,使用者互動就能更快執行,進而提升輸入回應速度和 INP。

明確讓步時,您會告訴瀏覽器:「我知道我即將執行的工作可能需要一段時間,而且我不想讓您在回應使用者輸入內容或其他可能也很重要的工作之前,必須完成所有工作」。這是開發人員工具箱中相當實用的工具,有助於大幅提升使用者體驗。

目前收益策略的問題

產生 的常見方法是使用 setTimeout,並將逾時值設為 0。這是因為傳遞至 setTimeout 的回呼會將剩餘工作移至另一個工作,並排入佇列以供後續執行。您不必等待瀏覽器自行產生結果,而是要求「將這項大型工作拆成多個小部分」。

不過,使用 setTimeout 產生可能會造成不良的副作用:在產生點之後的工作會移至工作佇列的後方。使用者互動排定的工作仍會如常移至佇列前端,但您在明確產生後想執行的其餘工作,可能會因為排在前面的其他來源工作而進一步延遲。

如要實際查看,請試用這個 Codepen 試用版,或在下列內嵌版本中進行實驗。這個範例包含幾個可點選的按鈕,以及下方的一個方塊,用於記錄工作執行時間。進入該頁面後,請執行下列動作:

  1. 按一下標示為「定期執行工作」的頂端按鈕,即可排定每隔一段時間執行封鎖工作。點選這個按鈕後,工作記錄會填入多則訊息,內容為「Ran blocking task with setInterval」(使用 setInterval 執行封鎖工作)
  2. 接著,按一下標示為「Run loop, yielding with setTimeout on each iteration」(執行迴圈,在每次疊代時產生 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 的實際運作情況,請按照下列步驟操作:

  1. 前往 chrome://flags
  2. 啟用「Experimental Web Platform features」實驗。完成後可能需要重新啟動 Chrome。
  3. 前往示範頁面,或使用下列內嵌版本。
  4. 按一下標示為「定期執行工作」的頂端按鈕。
  5. 最後,按一下標示為「Run loop, yielding with scheduler.yield on each iteration」(執行迴圈,在每次疊代時產生 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 版起):

  1. 如要在本機實驗 scheduler.yield,請在 Chrome 的網址列中輸入並按下 chrome://flags,然後在「Experimental Web Platform Features」(實驗性網頁平台功能) 區段的下拉式選單中選取「Enable」(啟用)。這樣一來,只有您使用的 Chrome 執行個體會啟用 scheduler.yield (和其他實驗功能)。
  2. 如要在公開可存取的來源上為實際 Chromium 使用者啟用 scheduler.yield,請註冊 scheduler.yield 來源試用。您可以在一段時間內安全地試用建議功能,並為 Chrome 團隊提供寶貴的洞察資料,瞭解這些功能在實際使用時的狀況。如要進一步瞭解來源試用計畫的運作方式,請參閱這份指南

如何使用 scheduler.yield (同時支援未導入這項功能的瀏覽器),取決於您的目標。您可以使用官方 Polyfill。如果符合下列情況,您可以使用 Polyfill:

  1. 您已在應用程式中使用 scheduler.postTask 排定工作。
  2. 您希望能夠設定工作和產生優先順序。
  3. 您希望能夠使用 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,並提供意見回饋,說明如何進一步提升 API 品質。

主頁橫幅圖片來自 Unsplash,由 Jonathan Allison 拍攝。