Chrome 88 (2021 年 1 月) 會在特定情況下,針對隱藏的網頁大幅降低鏈結式 JavaScript 計時器的速度。這麼做可降低 CPU 使用率,進而減少電池用量。在某些極端情況下,這會導致行為改變,但在使用其他 API 時,定時器通常會更有效率、更可靠。
好的,那是相當專業的術語,而且有點模稜兩可。我們來深入探討一下…
術語
隱藏的網頁
一般來說,「隱藏」是指其他分頁處於活動狀態,或是視窗已最小化,但瀏覽器可能會將內容完全無法顯示的頁面視為隱藏狀態。部分瀏覽器在這方面比其他瀏覽器更進步,但您隨時可以使用 page visibility API 追蹤瀏覽器認為可見度已變更的時間。
JavaScript 計時器
所謂的 JavaScript 計時器是指 setTimeout
和 setInterval
,可讓您在日後安排回呼。計時器很實用,而且不會消失,但有時會用於在事件更有效率且更準確時,對狀態進行輪詢。
連鎖計時器
如果您在同一個工作中呼叫 setTimeout
和 setTimeout
回呼,第二次叫用會「鏈結」。使用 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 的功能,讓裝置在發生變更時通知您,因此您不必一直檢查。看看是否有其他事件可達到相同的效果。
以下提供一些例子:
- 如果您需要瞭解元素何時進入可視區域,請使用
IntersectionObserver
。 - 如果您需要知道元素何時變更大小,請使用
ResizeObserver
。 - 如果您需要瞭解 DOM 變更的時間,請使用
MutationObserver
,或許也可以使用自訂元素生命週期回呼。 - 請改用 網頁連接埠、伺服器傳送的事件、推送訊息或擷取串流,而非輪詢伺服器。
- 如果您需要對音訊/影像的階段變更做出反應,請使用
timeupdate
和ended
等事件,或是requestVideoFrameCallback
來處理每個影格。
如要指定特定時間顯示通知,也可以使用通知觸發事件。
動畫
動畫是視覺效果,因此在網頁隱藏時,不應使用 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。