Chrome 88(2021 年 1 月)将在特定情况下大幅节流隐藏页面的链式 JavaScript 计时器。这将降低 CPU 用量,从而降低电池用量。在某些极端情况下,这会改变行为,但在其他 API 更高效、更可靠的情况下,通常会使用计时器。
好的,这些术语比较多,而且有点模糊。我们来深入了解一下…
术语
隐藏的网页
通常,“隐藏”表示其他标签页处于活动状态,或窗口已最小化,但每当网页的内容完全不可见时,浏览器都可能会将其视为隐藏。在这方面,有些浏览器的做法更为深入,但您始终可以使用页面可见性 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
具有“打开”RTCDataChannel
或“实时”MediaStreamTrack
。
浏览器每 1 秒检查一次此组中的计时器。由于每秒只检查一次,因此具有类似超时设置的计时器会一起批处理,从而缩短标签页运行代码所需的时间。这也不是什么新鲜事;多年来,浏览器在某种程度上一直在这样做。
强制节流
好的,下面介绍一下 Chrome 88 中的新内容。当不满足任何最低节流或节流条件,并且满足以下所有条件时,系统会对已调度的计时器进行强制节流:
- 相应网页已隐藏超过 5 分钟。
- 链接数为 5 或更大。
- 页面已静音至少 30 秒。
- WebRTC 未在使用中。
在这种情况下,浏览器每 1 分钟检查一次此组中的计时器。与之前一样,这意味着计时器会在这些每分钟的检查中批量处理。
解决方法
通常,有更好的替代计时器的方法,或者可以将计时器与其他内容结合使用,以减少对 CPU 和电池续航时间的影响。
状态轮询
这是计时器最常见的(误用)用法,即用于持续检查或轮询,以了解是否发生了变化。在大多数情况下,系统会提供等效的推送功能,让设备在发生变化时通知您,这样您就不必一直查看。看看是否有其他事件可以实现相同的目的。
一些示例:
- 如果您需要知道元素何时进入视口,请使用
IntersectionObserver
。 - 如果您需要知道元素何时更改大小,请使用
ResizeObserver
。 - 如果您需要知道 DOM 何时发生更改,请使用
MutationObserver
,或者使用自定义元素生命周期回调。 - 请考虑使用网络套接字、服务器发送的事件、推送消息或提取数据流,而不是轮询服务器。
- 如果您需要对音频/视频中的阶段变化做出响应,请使用
timeupdate
和ended
等事件;如果您需要对每个帧执行操作,请使用requestVideoFrameCallback
。
如果您想在特定时间显示通知,还可以使用通知触发器。
动画
动画是视觉效果,因此在网页隐藏时,动画不应使用 CPU 时间。
与 JavaScript 计时器相比,requestAnimationFrame
在安排动画工作方面要出色得多。它会与设备的刷新率同步,确保您每帧可显示的帧数只有一个,并且您有足够的时间来构建该帧。此外,requestAnimationFrame
会等待页面可见,因此在页面处于隐藏状态时不会使用任何 CPU。
如果您可以预先声明整个动画,请考虑使用 CSS 动画或 Web 动画 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 88(2021 年 1 月)中面向所有 Chrome 用户启用这项变更。 目前,Chrome Beta 版、开发者版和 Canary 版用户中有 50% 已启用此功能。如果您想对其进行测试,请在启动 Chrome Beta 版、Dev 版或 Canary 版时使用以下命令行标志:
--enable-features="IntensiveWakeUpThrottling:grace_period_seconds/10,OptOutZeroTimeoutTimersFromThrottling,AllowAggressiveThrottlingWithWebSocket"
grace_period_seconds/10
参数会导致在网页隐藏 10 秒后(而不是完整的 5 分钟后)触发强制节流,这样更容易查看节流的影响。
未来展望
由于计时器是导致 CPU 使用过多的一个原因,因此我们将继续研究在不破坏 Web 内容的情况下如何节流计时器,以及可以添加/更改哪些 API 来满足用例。我个人希望消除对 animationInterval
的需求,改用高效的低频动画回调。如果您有任何疑问,请在 Twitter 上与我联系!
标题图片由 Heather Zabriskie 在 Unsplash 上发布。