从 Chrome 88 开始,系统会对链接的 JS 计时器施加严格的节流限制

Jake Archibald
Jake Archibald

Chrome 88(2021 年 1 月)将在特定情况下大幅节流隐藏页面的链式 JavaScript 计时器。这将降低 CPU 用量,从而降低电池用量。在某些极端情况下,这会改变行为,但在其他 API 更高效、更可靠的情况下,通常会使用计时器。

好的,这些术语比较多,而且有点模糊。我们来深入了解一下…

术语

隐藏的网页

通常,“隐藏”表示其他标签页处于活动状态,或窗口已最小化,但每当网页的内容完全不可见时,浏览器都可能会将其视为隐藏。在这方面,有些浏览器的做法更为深入,但您始终可以使用页面可见性 API 来跟踪浏览器认为可见性发生变化的时间。

JavaScript 计时器

这里所说的 JavaScript 计时器是指 setTimeoutsetInterval,它们可让您安排在未来某个时间调用回调。计时器很有用,而且不会消失,但有时,使用事件进行轮询状态会更高效、更准确。

链式计时器

如果您在 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 和电池续航时间的影响。

状态轮询

这是计时器最常见的(误用)用法,即用于持续检查或轮询,以了解是否发生了变化。在大多数情况下,系统会提供等效的推送功能,让设备在发生变化时通知您,这样您就不必一直查看。看看是否有其他事件可以实现相同的目的。

一些示例:

如果您想在特定时间显示通知,还可以使用通知触发器

动画

动画是视觉效果,因此在网页隐藏时,动画不应使用 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 ZabriskieUnsplash 上发布。