为什么浏览器要限制 JavaScript 定时器?

原文:nolanlawson.com/2025/08/31/...

作者:Nolan

翻译:安东尼

前端周刊进群:flowus.cn/48d73381-69...

p1

即便你已经玩转 JavaScript 有一段时间,也许你会对 setTimeout(0) 不是字面意义的 setTimeout(0) 感到惊讶。实际上,它通常会延迟大约 4 毫秒后执行:

ini 复制代码
const start = performance.now();
setTimeout(() => {
  // 实际延迟大约 4 毫秒
  console.log(performance.now() - start);
}, 0);

十年前,我在 Microsoft Edge 团队时,有人向我解释过,浏览器之所以这样做,是为了避免滥用。也就是说,许多网站过度使用 setTimeout,为了避免耗尽用户的电池电量或阻塞交互,浏览器对其做了限制,设置了 4 毫秒的最小延迟。

这也能解释为什么某些浏览器在电池供电的设备上提高了限制(例如,旧版 Edge 为 16 毫秒),或者对后台标签页设置了更严格的限制(在 Chrome 中,甚至达到 1 秒!)。

然而,有一个问题困扰了我:既然 setTimeout 经常被滥用,为什么浏览器还不断引入新型定时器,比如 setImmediate(已废弃)、Promise,甚至是新兴的 scheduler.postTask()?如果 setTimeout 必须"被削弱",那么这些新定时器是不是也会走上同样的命运?

p2

2018 年,我曾写过一篇关于 JavaScript 定时器的长文,但直到最近我才有机会再次深入探讨这个问题。那时,我正在做一个名为 fake-indexeddb 的项目,这是一个纯 JavaScript 实现的 IndexedDB API,问题随之浮现。事实上,IndexedDB 需要在事件循环没有其他任务时自动提交事务------换句话说,在所有微任务完成后,且任何宏任务(我能不小心说"宏任务"吗?)开始之前。

为了实现这一点,fake-indexeddb 在 Node.js 中使用了 setImmediate(这与旧版浏览器实现相似),而在浏览器中则使用了 setTimeout。在 Node.js 中,setImmediate 很完美,因为它在微任务之后执行,但在任何其他任务之前执行,并且没有限制。但在浏览器中,setTimeout 的表现却相当糟糕:在基准测试中,我看到 Chrome 花费了 4.8 秒,而相同的操作在 Node 中仅需 300 毫秒(差距高达 16 倍!)。

展望 2025 年的定时器景观,选择哪个定时器并不显而易见。一些选项包括:

  • setImmediate --- 仅支持遗留版 Edge 和 IE,无法使用。
  • MessageChannel.postMessage --- 这是 afterframe 使用的技术。
  • window.postMessage --- 很好的选择,但可能会与页面上的其他脚本发生冲突。尽管如此,它仍被 setImmediate polyfill 使用。
  • scheduler.postTask --- 如果你不想再继续找了,这就是目前的赢家!但让我解释一下为什么!

为了比较这些选项,我编写了一个快速基准测试。以下是一些关键的测试说明:

  • 你需要运行多次 setTimeout(以及其他定时器)才能真正看出限制。根据 HTML 规范,4 毫秒的限制应在嵌套调用 setTimeout 时触发(即一个 setTimeout 调用另一个 setTimeout),通常需要 5 次调用才能生效。
  • 我没有测试每一个可能的组合:1)电池与插电、2)显示器刷新率、3)后台与前台标签等,尽管我知道这些因素会影响限制。我还有生活,尽管做实验很有趣,但我不想把整个周六都花在那上面。

无论如何,这里是数据(单位:毫秒,101 次迭代的中位数,2021 款 16 寸 MacBook Pro):

浏览器 setTimeout MessageChannel window scheduler.postTask
Chrome 139 4.2 0.05 0.03
Firefox 142 4.72 0.02 0.01
Safari 18.4 26.73 0.52 未实现

注:这个基准测试不容易编写!当我第一次写它时,我使用了 Promise.all 来同时运行所有定时器,但这破坏了 Safari 的嵌套启发式算法,并导致 Firefox 行为不稳定。现在的基准是独立运行每个定时器。

不必太纠结精确数字:重点是,Chrome 和 Firefox 将 setTimeout 限制为 4 毫秒,而其他三个选项表现差不多。在 Safari 中,setTimeout 被限制得更严格,而 MessageChannel.postMessagewindow.postMessage 稍慢(尽管 window.postMessage 由于上面提到的原因,表现不尽如人意)。

这个实验解答了我关于 fake-indexeddb 的即时问题:它应该使用 scheduler.postTask(因为它更易于使用),并回退到 MessageChannel.postMessagewindow.postMessage。我确实试验过 postTask 的不同优先级,但它们表现几乎相同。对于 fake-indexeddb 的用例,默认的 user-visible 优先级最合适。

p3

然而,这并没有解决我最初的问题:既然 Web 开发者可以使用 scheduler.postTaskMessageChannel,为什么浏览器还要限制 setTimeout 呢?我问了我的朋友 Todd Reifsteck,他曾是 Web 性能工作组的联合主席,并参与了关于"干预"措施的许多工作。

他说,实际上有两种阵营:一方认为需要限制定时器,以防 Web 开发者自食其果;另一方则认为开发者应该为"愚蠢"行为负责,任何微妙的限制启发式算法都会带来混乱。简而言之,这是性能 API 设计中的标准权衡:某些 API 速度很快,但可能带来意外的风险。

这与我对问题的直觉一致。浏览器的干预往往是因为 Web 开发者使用了过多的有用工具(例如 setTimeout),或者对更好的选项不了解(就像触摸监听器争议那样)。最终,浏览器充当了"用户代理",代表用户行事,W3C 的优先级也明确表明,最终用户的需求永远优先于开发者的需求。

话虽如此,Web 开发者通常希望做对的事。(我认为这篇博客也是朝这个方向努力的。)不过,我们并不总是有足够的工具来实现这一目标,因此经常不得不拿起手边的"钝器"开始工作。如果我们能拥有更多对任务和调度的控制能力,或许就能避免用 setTimeout 强行干预,减少浏览器的"必要干预"。

我预测,postTask/postMessage 将暂时不受限制。根据 Todd 的"两种阵营"理论,Scheduler API 的出现似乎是"支持控制"阵营的胜利,当前正由他们主导。尽管 Todd 认为这也是两个阵营的妥协:是的,它提供了更多控制,但它也与浏览器的实际渲染管道更好地对接,而非任意超时。

但我内心的悲观主义者担心,API 依然可能会被滥用------例如,无差别地在所有地方使用用户阻塞优先级。也许未来某个创新的浏览器供应商会加大限制力度,发现这样可以让网站更快、更响应、并且更省电。如果发生这种情况,我们可能会再经历一轮"干预"(也许我们需要一个 scheduler2 API 来帮助我们摆脱困境!)。

目前,我并不深度参与 Web 标准的制定,只能推测。对于我来说,像大多数 Web 开发者一样,选择今天可用的 API 来完成目标,并希望浏览器未来不要做太多变动。只要我们小心,不引入过多"愚蠢"行为,我觉得这不算过分的要求。

特别感谢 Todd Reifsteck 对本文草稿的反馈。

注:在讨论 setTimeout 时,我实际上也可以将其视为 setInterval。从浏览器角度看,这两个 API 几乎是一样的。

相关推荐
Wiktok2 小时前
pureadmin的动态路由和静态路由
前端·vue3·pureadmin
devii662 小时前
html.
前端
学前端搞口饭吃2 小时前
react context如何使用
前端·javascript·react.js
GDAL2 小时前
为什么Cesium不使用vue或者react,而是 保留 Knockout
前端·vue.js·react.js
IT_陈寒2 小时前
《Java 21新特性实战:5个必学的性能优化技巧让你的应用快30%》
前端·人工智能·后端
小谭鸡米花2 小时前
uni小程序中使用Echarts图表
前端·小程序·echarts
芜青2 小时前
【Vue2手录11】Vue脚手架(@vue_cli)详解(环境搭建+项目开发示例)
前端·javascript·vue.js
a别念m2 小时前
前端架构-CSR、SSR 和 SSG
前端·架构·前端框架
东方芷兰3 小时前
Leetcode 刷题记录 21 —— 技巧
java·算法·leetcode·职场和发展·github·idea