使用 isInputPending() 改善 JS 排程

新的 JavaScript API,可避免您在載入效能和輸入回應速度之間做出取捨。

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

快速載入很困難。目前,使用 JS 算繪內容的網站必須在載入效能和輸入回應性之間做出取捨:要不就是一次執行所有顯示作業所需的所有工作 (載入效能較佳,輸入回應性較差),要不就是將工作分割成較小的任務,以便持續回應輸入和繪製作業 (載入效能較差,輸入回應性較佳)。

為避免需要做出這種取捨,Facebook 在 Chromium 中提出並實作 isInputPending() API,以便在不產生讓步的情況下改善回應速度。根據來源試用版的意見回饋,我們對 API 進行了多項更新,很高興在此宣布,API 現已預設提供於 Chromium 87!

瀏覽器相容性

瀏覽器支援

  • Chrome:87。
  • 邊緣:87。
  • Firefox:不支援。
  • Safari:不支援。

資料來源

isInputPending() 已在 87 以上版本的 Chromium 瀏覽器中提供。其他瀏覽器都沒有傳送意圖,表示要發布 API。

背景

現今 JS 生態系統中的大部分工作都是在單一執行緒 (主執行緒) 上完成。這可為開發人員提供可靠的執行模型,但如果指令碼執行時間過長,使用者體驗 (特別是回應速度) 可能會大受影響。舉例來說,如果網頁在觸發輸入事件時正在執行大量工作,則網頁會在該項工作完成後才處理點擊輸入事件。

目前的最佳做法是將 JavaScript 分割成較小的區塊,以便處理這個問題。在網頁載入期間,網頁可以執行一些 JavaScript,然後交出並將控制權交還給瀏覽器。瀏覽器接著可以檢查輸入事件佇列,看看是否有任何需要告知網頁的內容。接著,瀏覽器就可以在新增 JavaScript 區塊時,繼續執行這些區塊。這麼做有助於解決問題,但可能會導致其他問題。

每次網頁將控制權交還給瀏覽器時,瀏覽器都需要花費一些時間檢查輸入事件佇列、處理事件,以及接收下一個 JavaScript 區塊。雖然瀏覽器回應事件的速度加快,但網頁的整體載入時間會變慢。如果我們太常讓出控制權,頁面載入速度就會過慢。如果我們較少讓出控制權,瀏覽器就需要更長的時間回應使用者事件,使用者也會感到不耐煩。這一點都不好玩。

這張圖表顯示,當您執行長時間的 JS 工作時,瀏覽器可用來調度事件的時間較少。

我們希望在 Facebook 上,看看如果我們採用新的載入方式,可以消除這種令人沮喪的權衡,情況會如何。我們向 Chrome 團隊提出這項提案,並提出 isInputPending() 的提案。isInputPending() API 是第一個使用網頁上使用者輸入中斷概念的 API,可讓 JavaScript 檢查輸入內容,而無須交由瀏覽器處理。

這張圖表顯示 isInputPending() 可讓 JS 檢查是否有待處理的使用者輸入內容,而不必將執行作業完全交由瀏覽器處理。

由於 API 受到關注,我們與 Chrome 的同事合作,在 Chromium 中實作並發布這項功能。在 Chrome 工程師的協助下,我們已在來源試用版中推出修補程式 (這是 Chrome 在完全發布 API 前,測試變更並取得開發人員意見的一種方式)。

我們已參考來源測試和 W3C 網頁效能工作小組其他成員的意見,並對 API 實施變更。

範例:更有效率的排程器

假設您需要執行許多顯示阻斷作業才能載入網頁,例如從元件產生標記、分離素數,或只是繪製酷炫的載入旋轉圖示。每個項目都會細分為個別的工作項目。使用排程器模式,讓我們勾勒出如何在假設的 processWorkQueue() 函式中處理工作:

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

透過 setTimeout() 在稍後透過新宏作業叫用 processWorkQueue(),可讓瀏覽器在管理相對不受干擾的執行作業時,仍能對輸入內容做出某種回應 (可在工作恢復前執行事件處理常式)。不過,如果有其他工作需要控制事件迴圈,可能會讓我們長時間處於取消排程狀態,或是導致事件延遲時間增加至 QUANTUM 毫秒。

這還不錯,但我們可以做得更好嗎?當然可以!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

透過呼叫 navigator.scheduling.isInputPending(),我們可以更快回應輸入內容,同時確保顯示封鎖作業不會中斷。如果我們不想在工作完成前處理輸入內容以外的任何內容 (例如繪圖),也可以輕鬆增加 QUANTUM 的長度。

根據預設,系統不會從 isInputPending() 傳回「連續」事件。包括 mousemovepointermove 和其他頻道。如果您也對這些項目感興趣,我們也樂意提供收益。只要將 includeContinuous 設為 true,即可為 isInputPending() 提供物件,讓我們繼續進行:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

大功告成!React 等架構會使用類似的邏輯,在核心排程程式庫中建構 isInputPending() 支援功能。希望這項功能能讓使用這些架構的開發人員在不需要大幅重寫的情況下,就能在幕後享有 isInputPending() 的優點。

讓出資源不一定是壞事

值得注意的是,減少產量並非適用於所有用途的正確解決方案。除了處理輸入事件,還有許多原因會將控制權交還給瀏覽器,例如在網頁上執行算繪和其他指令碼。

在某些情況下,瀏覽器無法正確歸因待處理的輸入事件。特別是,為跨來源 iframe 設定複雜的片段和遮罩可能會回報假陰性 (也就是說,isInputPending() 在指定這些框架時可能會意外傳回 false)。如果您的網站確實需要與圖示化子畫面互動,請務必確保您定期產生收益。

請留意其他共用事件迴圈的網頁。在 Android 版 Chrome 等平台上,多個來源共用事件迴圈的情況相當常見。如果輸入內容會傳送至跨來源框架,isInputPending() 就不會傳回 true,因此背景頁面可能會影響前景頁面的回應速度。您可能會想在使用 Page Visibility API 執行背景工作時,更頻繁地減少、延後或讓出工作。

建議您謹慎使用 isInputPending()。如果沒有需要阻斷使用者的作業,請盡量頻繁地在事件迴圈中產生,以免影響其他人。長時間的工作可能會造成不良影響

意見回饋

結論

我們很高興 isInputPending() 即將推出,開發人員可以立即開始使用。這是 Facebook 首次建構新的網頁 API,並將其從構思孵育階段,轉變為標準提案,進而實際在瀏覽器中推出。在此感謝所有協助我們完成這項功能的人員,特別感謝 Chrome 團隊成員,感謝你們協助我們實現這個構想,並將其推向市場!

主頁橫幅相片由 Will H McMahan 提供,取自 Unsplash