trainr.yield 來源試用簡介

建立能迅速回應使用者輸入內容的網站,一直是網頁效能最具挑戰性的一環,Chrome 團隊也一直致力於協助網頁開發人員克服這個難題。今年稍早,我們宣布「與下一個顯示的內容互動」指標將從實驗狀態轉為待定狀態。如今,INP 已準備好在 2024 年 3 月取代 First Input Delay (FID),成為 Core Web Vitals 之一。

為了持續提供新的 API,協助網頁開發人員讓網站盡可能快速,Chrome 團隊目前正在 Chrome 115 版中執行 scheduler.yield 的來源試用版scheduler.yield 是排程器 API 建議的新功能,可提供比傳統方法更簡單、更優良的方式,將控制權交還給主執行緒。

在產生時

JavaScript 會使用執行至完成的模式處理工作。也就是說,當工作在主執行緒上執行時,該工作會在完成所需的時間內執行。工作完成後,系統會將控制權交出至主執行緒,讓主執行緒處理佇列中的下一個工作。

除了工作永遠無法完成的極端情況 (例如無限迴圈) 之外,JavaScript 的工作排程邏輯不可避免地會產生交出動作。一定會發生,只是時間問題,越早越好。如果工作耗時過長 (具體來說,超過 50 毫秒),系統會將其視為長時間工作

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

不過,即使程式碼在瀏覽器中啟動工作,也不代表您必須等到該工作完成,才能將控制權交還給主執行緒。您可以透過在工作中明確地交出,改善對網頁上使用者輸入內容的回應速度,這會將工作分割成可在下次可用機會完成的工作。這樣一來,其他工作就能更快取得主執行緒的時間,不必等待長時間的工作完成。

這張圖片說明如何分割工作,以便提高輸入回應速度。在頂端,長時間工作會阻斷事件處理常式,直到工作完成為止。在底部,分割的工作可讓事件處理常式提早執行。
將控制權交還給主執行緒的視覺化效果。在頂層,系統只會在工作執行完畢後才會產生交出,這表示工作可能需要較長的時間才能完成,然後再將控制權交還給主執行緒。在底部,明確執行產生,將長時間工作拆分為多項較小的工作。這樣一來,使用者互動作業就能更快執行,進而改善輸入回應速度和 INP。

明確執行 yield 時,您會告訴瀏覽器:「我知道我要執行的工作可能需要一段時間,但我不希望您在回應使用者輸入內容或其他可能重要的工作之前,就必須執行所有這項工作。」這項工具是開發人員工具箱中的重要工具,可大幅改善使用者體驗。

目前收益策略的問題

產生 setTimeout 的常用方法,其逾時值為 0。這是因為傳遞至 setTimeout 的回呼會將剩餘的工作移至另一個工作,並將該工作排入後續執行的佇列。您不必等待瀏覽器自行產生,而是可以說「請將這項大工作拆成較小的部分」。

不過,使用 setTimeout 產生內容可能會帶來不必要的副作用:產生點「後」的工作會進入工作佇列的後方。由使用者互動排程的工作仍會如預期排入佇列,但您在明確讓步後想要執行的其他工作,可能會因排在前面的其他競爭來源而進一步延遲。

如要實際體驗這項功能,請試試這個 Glitch 示範,或在下方嵌入的版本中進行實驗。這個示範包含幾個可點選的按鈕,以及下方用於記錄工作執行時間的方塊。進入該頁面後,請執行下列操作:

  1. 按一下頂端的「定期執行工作」按鈕,系統就會排定每隔一段時間執行阻斷工作。點選這個按鈕後,工作記錄會填入多則訊息,內容為「Ran blocking task with setInterval
  2. 接著,按一下「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 的實際運作情形,請按照下列步驟操作:

  1. 前往 chrome://flags
  2. 啟用「實驗性 Web Platform 功能」實驗。完成後,你可能需要重新啟動 Chrome。
  3. 前往示範頁面,或使用清單下方的內嵌版本。
  4. 按一下頂端的「定期執行工作」按鈕。
  5. 最後,請按一下「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 版起提供兩種方式:

  1. 如果您想在本機測試 scheduler.yield,請在 Chrome 的網址列中輸入 chrome://flags,然後在「實驗性 Web 平台功能」部分的下拉式選單中選取「啟用」。這樣一來,scheduler.yield (以及任何其他實驗功能) 只會在您的 Chrome 例項中提供。
  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