使用 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:不支援。

資料來源

自 87 版起,isInputPending() 已於以 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