原文: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.postMessage
比 window.postMessage
稍慢(尽管 window.postMessage
由于上面提到的原因,表现不尽如人意)。
这个实验解答了我关于 fake-indexeddb 的即时问题:它应该使用 scheduler.postTask
(因为它更易于使用),并回退到 MessageChannel.postMessage
或 window.postMessage
。我确实试验过 postTask
的不同优先级,但它们表现几乎相同。对于 fake-indexeddb 的用例,默认的 user-visible
优先级最合适。
p3
然而,这并没有解决我最初的问题:既然 Web 开发者可以使用 scheduler.postTask
或 MessageChannel
,为什么浏览器还要限制 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 几乎是一样的。