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。

獲得明確結果時,系統是告知瀏覽器「嘿,我本來要做的事可能需要一點時間,我不希望在回應使用者輸入內容或其他重要工作之前,先執行所有工作」。它是開發人員工具箱中的重要工具,可有效改善使用者體驗。

目前收益策略的問題

常見的產生方法會使用逾時值為 0setTimeout。這項功能之所以有效,是因為傳遞至 setTimeout 的回呼會將剩餘的工作移至另外排入佇列,供後續執行的工作使用。與其等待瀏覽器自行產生版本,不如「讓我們把這塊龐大工作分拆成更小的點」。

但是,使用 setTimeout 產生可能會產生不想要的副作用,也就是收益點「之後」發生的工作會回到工作佇列的背面。由使用者互動排定的工作仍會如預期顯示在佇列最前方;不過,在明確得到這些工作後,如果您想執行其餘工作,最後可能會因為排隊其他競爭來源的工作而延後完成。

如要瞭解實際運作方式,請觀看此 Glitch 示範,或在下方嵌入版本試用看看。這個示範包含幾個按鈕,可供您點選,並在這些按鈕下方顯示記錄工作執行時的方塊。進入該網頁後,請執行下列動作:

  1. 按一下頂端標示為「定期執行任務」的按鈕,即可排定讓封鎖工作頻繁執行。您點選這個按鈕後,工作記錄會填入幾則訊息:使用 setInterval 執行封鎖工作
  2. 接著,按一下標示「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 的實際運作情形,請執行下列操作:

  1. 前往 chrome://flags
  2. 啟用「實驗性 Web Platform 功能」實驗。執行上述操作後,你可能必須重新啟動 Chrome。
  3. 請前往示範頁面或使用這份清單下方的嵌入版本。
  4. 按一下標有「定期執行工作」的按鈕,
  5. 最後,按一下標示為「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 版開始採用下列兩種方法:

  1. 如要在本機環境中使用 scheduler.yield 進行實驗,請在 Chrome 的網址列中輸入並輸入 chrome://flags,然後在「實驗性 Web Platform 功能」部分的下拉式選單中選取「啟用」。這會讓 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 的品質,並提供意見,協助我們改善這項功能。

主頁橫幅由 Jonathan AllisonUnsplash 提供。