trainr.yield 來源試用簡介

打造可快速回應使用者輸入的網站是網路效能最棘手的一點,Chrome 團隊一直努力幫助網頁開發人員達成這個目標。我們在今年宣布「與下一個顯示的內容互動」(INP) 指標將從實驗性功能升級為待處理狀態。目前已可在 2024 年 3 月將 First Input Delay (FID) 取代為 Core Web Vitals。

Chrome 團隊為了持續提供新的 API,協助網頁程式開發人員盡可能簡化網站,Chrome 團隊目前正從 Chrome 115 版開始執行 scheduler.yield 來源試用scheduler.yield 是排程器 API 的新增提議,可讓排程器 API 更輕鬆地傳回主執行緒,比傳統上依賴的方法更方便。

創造中

JavaScript 採用「執行中完成」模型來處理工作。這表示工作在主執行緒上執行時,該工作會長時間執行直到完成為止。工作完成後,控制項會「產生」回主執行緒,讓主執行緒處理佇列中的下一項工作。

除了工作永遠無法完成的極端情況 (例如無限迴圈) 以外,JavaScript 工作排程邏輯是不可避免的。只是「何時」會發生,只是因應「時機」而已,事不宜遲。無論工作執行時間為何 (超過 50 毫秒) 以上,這些工作都算是長時間工作

較長的工作會讓瀏覽器無法回應使用者輸入內容,造成網頁反應速度變慢。執行時間越長,且執行的時間越長,就越有可能讓使用者認為網頁很慢,或甚至覺得網頁完全毀損。

不過,就算程式碼會在瀏覽器中啟動某項任務,也不代表須等到工作完成之後,控制項才會傳回主執行緒。藉由在工作中明確產生工作,導致系統在下一個可用機會中斷完成某項任務,可提升使用者在網頁上輸入的內容回應速度。如此一來,比起必須等待長時間工作完成,其他工作就能更快在主執行緒上節省時間。

說明如何分解任務可以提高輸入回應能力。頂端的長時間工作會阻止事件處理常式執行,直到工作完成為止。底部的區塊化工作允許事件處理常式比其他工作更快執行。
將控制項傳回主執行緒的視覺化呈現。頂部來說,只有在工作執行完畢時才會產生結果,也就是說,工作可能需要較長時間完成,才能將控制權交還給主執行緒。最下方則是明確完成,將耗時的工作分割成數個較小的工作。這可讓使用者互動的執行速度更快,藉此改善輸入回應和 INP。

當你明確產生影片,這意味著我知道我打算做的工作可能需要一些時間,而我不希望在回應使用者輸入內容或涉及其他重要的工作之前,都執行「所有」作業。在開發人員工具箱中,這項實用工具可大幅改善使用者體驗。

目前收益策略相關問題

常見的產生方法使用 setTimeout,逾時值為 0。之所以能這麼做,是因為傳遞至 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. 按一下頂端標示為「Run tasksRegular」(定期執行) 的按鈕。
  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」,然後在「Experimental 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。

Jonathan Allison 撰寫的 Unsplash 主頁橫幅。