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. 啟用「實驗性網頁平台功能」實驗。完成後,你可能需要重新啟動 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 的品質,並提供意見,協助我們改善這項功能。

主頁橫幅圖片取自 Unsplash,圖片作者:Jonathan Allison