各位开发者朋友,不知道你有没有遇到过这种情况:你写了个 setTimeout(fn, 0),满心期待着它立刻执行,结果它却"磨蹭"了大概 4 毫秒才跑起来?别怀疑,这不是你的代码写错了,而是浏览器在背后悄悄"搞了小动作"!今天咱们就来聊聊,浏览器为啥要对这些定时器"节流"(throttle),以及面对这些限制,我们又能有哪些新选择。
⏰ 1. 定时器并不"准时"
如果你尝试过下面这段代码:
JavaScript
const start = performance.now();
setTimeout(() => {
console.log(performance.now() - start); // 输出大概是4毫秒
}, 0);
你会发现,即便是设置为 0 毫秒延迟的 setTimeout,实际执行也会花费大约 4 毫秒。这是因为浏览器为了避免开发者"滥用"定时器(比如疯狂循环调用 setTimeout),从而设置了最低 4 毫秒的延迟限制。这样一来,可以防止某些网站过度消耗用户的电池电量或者阻塞页面交互。
浏览器的"节流策略"还会因情况而变化:
- 设备使用电池时 :比如旧版的 Edge 浏览器会将延迟提升到 16 毫秒。
- 标签页处于后台时 :Chrome 对后台标签页的定时器延迟甚至可能达到 1 秒!😱
🤔 2. 既然节流,为何还出新 API?
看到这里,你可能会有个疑问:既然 setTimeout 因为被滥用而被限制了,那为什么浏览器们还在不断推出新的定时器 API 呢?比如已经"凉了"的 setImmediate,或是 Promise,又或是新秀 scheduler.postTask()?难道它们最终不会面临同样的"被节流"的命运吗?
这个问题也困扰了原文作者很久。直到他在开发一个纯 JavaScript 实现的 IndexedDB API(fake-indexeddb)时,才重新审视了这个问题。IndexedDB 希望在事件循环中没有未完成的任务时自动提交事务,换句话说,就是在所有微任务(microtasks)完成后,但任何宏任务(比方说 setTimeout 这样的"任务")开始之前。
为了模拟这个过程,fake-indexeddb 在 Node.js 中使用了 setImmediate(这很合适,因为它恰好在微任务之后、其他任务之前执行,且没有延迟限制),在浏览器中则不得不使用 setTimeout。结果呢?作者发现在 Chrome 中某个操作需要 4.8 秒,而在 Node 中仅需 300 毫秒(慢了 16 倍!) .
🧪 3. 2025 年,我们有哪些定时器选择?
既然 setTimeout 在浏览器中表现如此不尽人意,那在 2025 年的今天,我们有没有更好的选择呢?作者测试了一些方案:
浏览器 | setTimeout | MessageChannel | window.postMessage | scheduler.postTask |
---|---|---|---|---|
Chrome 139 | 4.2 ms | 0.05 ms | 0.03 ms | 0.00 ms |
Firefox 142 | 4.72 ms | 0.02 ms | 0.01 ms | 0.01 ms |
Safari 18.4 | 26.73 ms | 0.52 ms | 0.05 ms | 未实现 |
测试数据为 101 次迭代的中位数(单位:毫秒)
从表格中可以清晰地看到:
- setTimeout:在各个浏览器中都有明显的延迟(Safari 尤其突出)。
- MessageChannel.postMessage 和 window.postMessage:表现比 setTimeout 好很多,延迟显著降低。
- scheduler.postTask :在 Chrome 和 Firefox 中表现最佳,延迟极低甚至为 0,但目前 Safari 尚未实现。
所以,如果你现在需要高精度的定时器,scheduler.postTask 似乎是当下的最佳选择(当然,要考虑 Safari 的兼容性问题)。
💎 4. 总结与启示
- 浏览器对 setTimeout 和 setInterval 等定时器的节流 ,主要是出于性能和保护用户电池寿命的考虑,防止糟糕的代码拖垮整个浏览器或设备。
- 这种节流策略并非一成不变,它会根据设备是否接通电源、标签页是否处于后台等因素动态调整。
- 对于需要高精度定时 或对性能敏感的场景(比如实现动画、模拟 IndexedDB 事务等),传统的 setTimeout 可能不再是最佳选择。
- 可以关注并尝试使用新的 scheduler.postTask API(当然,要准备好回退方案),或者根据实际情况考虑 MessageChannel 等替代方案。
- 并非所有定时器任务都需要高精度。对于很多日常任务(如简单的延迟、轮询),setTimeout 和 setInterval 仍然完全够用且兼容性最好。
希望这篇译文能帮你理解浏览器定时器背后的"小秘密",以及如何在不同的场景下做出更合适的选择。如果你在项目中遇到过定时器的"坑",或者对新的定时器 API 有使用经验,欢迎在评论区分享交流! 🎉
原文链接:nolanlawson.com/2025/08/31/...
原文作者:Nolan Lawson