从 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。具体而言,有一个带有“open”RTCDataChannel 或“live”MediaStreamTrackRTCPeerConnection

浏览器将每检查一次此组中的计时器。由于它们每秒只会检查一次,因此具有类似超时的计时器会一起批处理,从而合并标签页运行代码所需的时间。这也不是什么新鲜事物 浏览器多年来一直在采用这种技术

密集型节流

好,下面为您介绍 Chrome 88 中的新部分。密集型节流适用于在不符合最低节流节流条件且满足以下所有条件时调度的计时器:

  • 该网页已被隐藏超过 5 分钟。
  • 链数为 5 或更大。
  • 网页已静音至少 30 秒。
  • WebRTC 未在使用。

在这种情况下,浏览器将每分钟检查一次此组中的计时器。与之前类似,这意味着计时器将在这些每分钟检查中分批进行。

解决方法

计时器通常有更好的替代方案,或者计时器可与其他内容结合使用,这样更利于 CPU 和电池续航时间。

状态轮询

这是计时器最常见的(滥用)使用方式。计时器用于不断检查或轮询,查看是否发生了更改。在大多数情况下,有一项等效的 push 操作,当发生更改时,内容会告知您更改的相关信息,因此您无需不断检查。看看是否有活动可实现同样的目的。

部分示例:

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

动画

动画是一种视觉元素,因此当页面隐藏时,动画不应该占用 CPU 时间。

与 JavaScript 计时器相比,requestAnimationFrame 更擅长调度动画工作。它与设备的刷新率同步,从而确保每个可显示的帧只获得一个回调,并为您构建该帧所用的时间最长。此外,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 88(2021 年 1 月)的所有 Chrome 用户启用。 目前,50% 的 Chrome Beta 版、开发者版和 Canary 版用户已启用该功能。如果要对其进行测试,请在启动 Chrome Beta 版、开发者版或 Canary 版时使用以下命令行 flag:

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

grace_period_seconds/10 参数会导致网页在隐藏 10 秒后(而不是完整的 5 分钟)后启动,从而让您更轻松地了解限制的影响。

未来展望

由于计时器是导致 CPU 过度使用的原因之一,因此我们将继续寻找在不破坏 Web 内容的情况下限制计时器的方法,以及我们可以添加/更改 API 来满足用例要求的方法。就我个人而言,我想要消除对 animationInterval 的需求,改为使用高效的低频动画回调。如果您有任何疑问,请在 Twitter 上与我联系

标题照片:Unsplash 用户 Heather Zabriskie