Chrome 第 88 版起針對鏈結的 JS 計時器進行大量節流

Jake Archibald
Jake Archibald

Chrome 88 (2021 年 1 月) 會在特定情況下,針對隱藏的網頁大幅降低鏈結式 JavaScript 計時器的速度。這麼做可降低 CPU 使用率,進而減少電池用量。在某些極端情況下,這會導致行為改變,但在使用其他 API 時,定時器通常會更有效率、更可靠。

好的,那是相當專業的術語,而且有點模稜兩可。我們來深入探討一下…

術語

隱藏的網頁

一般來說,「隱藏」是指其他分頁處於活動狀態,或是視窗已最小化,但瀏覽器可能會將內容完全無法顯示的頁面視為隱藏狀態。部分瀏覽器在這方面比其他瀏覽器更進步,但您隨時可以使用 page visibility API 追蹤瀏覽器認為可見度已變更的時間。

JavaScript 計時器

所謂的 JavaScript 計時器是指 setTimeoutsetInterval,可讓您在日後安排回呼。計時器很實用,而且不會消失,但有時會用於在事件更有效率且更準確時,對狀態進行輪詢。

連鎖計時器

如果您在同一個工作中呼叫 setTimeoutsetTimeout 回呼,第二次叫用會「鏈結」。使用 setInterval 時,每個疊代都是鏈結的一部分。以下程式碼可能比較容易理解:

let chainCount = 0;

setInterval(() => {
  chainCount++;
  console.log(`This is number ${chainCount} in the chain`);
}, 500);

且:

let chainCount = 0;

function setTimeoutChain() {
  setTimeout(() => {
    chainCount++;
    console.log(`This is number ${chainCount} in the chain`);
    setTimeoutChain();
  }, 500);
}

節流的運作方式

節流會分階段進行:

最小節流

這會發生在下列「任一」條件成立時,已排定的計時器:

  • 網頁可見
  • 網頁在過去 30 秒內發出噪音。這可以來自任何產生聲音的 API,但靜音音軌不算在內。

除非要求的逾時時間小於 4 毫秒,且鏈結數量為 5 以上,否則計時器不會受到節流限制。在這種情況下,逾時時間會設為 4 毫秒。這並非新功能,瀏覽器已實作多年。

調節

這會發生在以下情況:最小限流量控制不適用,且下列任一條件成立:

  • 鏈結數少於 5 個。
  • 網頁隱藏時間不到 5 分鐘。
  • 正在使用 WebRTC。具體來說,有一個 RTCPeerConnection 包含「open」RTCDataChannel或「live」MediaStreamTrack

瀏覽器會每檢查一次這個群組中的計時器。由於每秒只會檢查一次,因此具有類似逾時時間的計時器會分批處理,整合分頁執行程式碼所需的時間。這也不是什麼新鮮事,瀏覽器多年以來一直在某種程度上執行這項操作。

密集節流

好的,以下是 Chrome 88 的新功能。在沒有任何最小節流節流條件適用的情況下,如果排程的計時器符合下列所有條件,就會發生密集節流:

  • 該頁面已隱藏超過 5 分鐘。
  • 鏈結數量為 5 以上。
  • 網頁至少靜默 30 秒。
  • 未使用 WebRTC。

在這種情況下,瀏覽器會每分鐘檢查一次這個群組中的計時器。與先前類似,這表示計時器會在每分鐘檢查中一起執行。

解決方法

通常有更好的替代方案可取代計時器,或者您可以將計時器與其他項目搭配使用,以便善用 CPU 和電池續航力。

狀態輪詢

這是計時器最常見的用法 (誤用),也就是持續檢查或輪詢系統,查看是否有任何變更。在大多數情況下,系統會提供類似push 的功能,讓裝置在發生變更時通知您,因此您不必一直檢查。看看是否有其他事件可達到相同的效果。

以下提供一些例子:

如要指定特定時間顯示通知,也可以使用通知觸發事件

動畫

動畫是視覺效果,因此在網頁隱藏時,不應使用 CPU 時間。

requestAnimationFrame 比 JavaScript 計時器更適合安排動畫工作。這項功能會與裝置的更新率同步,確保每個可顯示的框架只會收到一個回呼,且您可獲得建構該框架的最大時間。此外,requestAnimationFrame 會等待網頁顯示,因此在網頁隱藏時不會使用任何 CPU。

如果您可以預先宣告整個動畫,建議使用 CSS 動畫網頁動畫 API。這些方法與 requestAnimationFrame 有相同的優點,但瀏覽器可以執行額外的最佳化作業 (例如自動合成),而且通常更容易使用。

如果動畫的幀率較低 (例如閃爍的游標),目前仍建議使用計時器,但您可以將計時器與 requestAnimationFrame 結合,以便同時享有兩者的優點:

function animationInterval(ms, signal, callback) {
  const start = document.timeline.currentTime;

  function frame(time) {
    if (signal.aborted) return;
    callback(time);
    scheduleFrame(time);
  }

  function scheduleFrame(time) {
    const elapsed = time - start;
    const roundedElapsed = Math.round(elapsed / ms) * ms;
    const targetNext = start + roundedElapsed + ms;
    const delay = targetNext - performance.now();
    setTimeout(() => requestAnimationFrame(frame), delay);
  }

  scheduleFrame(start);
}

使用方式:

const controller = new AbortController();

// Create an animation callback every second:
animationInterval(1000, controller.signal, time => {
  console.log('tick!', time);
});

// And stop it:
controller.abort();

測試

所有 Chrome 使用者都會在 Chrome 88 中 (2021 年 1 月) 啟用這項變更。目前已為 50% 的 Chrome Beta 版、開發人員版和 Canary 版使用者啟用這項功能。如要進行測試,請在啟動 Chrome Beta、Dev 或 Canary 時使用這個指令列旗標:

--enable-features="IntensiveWakeUpThrottling:grace_period_seconds/10,OptOutZeroTimeoutTimersFromThrottling,AllowAggressiveThrottlingWithWebSocket"

grace_period_seconds/10 引數會在頁面隱藏 10 秒後啟動密集節流,而非整整 5 分鐘,因此更容易觀察節流的影響。

未來

由於計時器會造成 CPU 使用量過高,我們會繼續尋找在不破壞網頁內容的情況下,限制計時器的使用量,以及新增/變更可滿足用途的 API。就我個人而言,我希望能消除對 animationInterval 的需求,改用效率較高的低頻率動畫回呼。如有任何問題,請透過 Twitter 與我聯絡

頁首相片由 Heather Zabriskie 提供,取自 Unsplash