第6次起settimeout(0)最小延迟强制≥4ms,是html标准规定;前5次约0.3–1.2ms,第6层起跳至4–6ms;层级按宏任务调用链累计,与缩进无关;非活跃标签页延迟升至1000ms或10000ms。

嵌套第 6 次起 setTimeout(0) 就不是 0ms 了
浏览器强制把第 6 层及更深的 setTimeout 最小延迟拉到 ≥4ms,不是 bug,是 HTML 标准硬性规定。前 5 次调用可能仍接近 0ms(实测常为 0.3–1.2ms),但从第 6 次开始,performance.now() 差值会突然跳到 4–6ms。
- 这个“层级”指连续递归调用的深度,不是代码缩进或函数嵌套层数
- 每次
setTimeout调用都算一层,哪怕中间穿插了Promise.resolve().then()或其他异步操作 - Chrome、Firefox、Edge 当前版本均遵守该规则,Safari 行为类似但未完全对齐标准
为什么是 6 层而不是 5 层或 10 层
HTML 规范里写的是「嵌套层级 ≥ 6」触发节流,本质是防止单个脚本通过无限 setTimeout(0) 抢占主线程。早期实现(如 Chrome 2012 年版本)就设了 5 层缓冲,第 6 层开始限速,后来被纳入标准。
- 它不关心你是否在做有用的事,只看调用链长度:A → B → C → D → E → F,F 就是第 6 层
- 如果你用
setImmediate(已废弃)或queueMicrotask混搭,不会重置计数——层级是按宏任务调度路径累计的 - 服务端 Node.js 不受此限,
setTimeout(0)可低至 0.1ms,但这是不同运行时
非活动标签页会让 4ms 变成 1000ms 甚至 10000ms
用户切走标签页后,浏览器会进一步降级定时器精度,这不是嵌套问题,但常被误认为是同一类现象。
- Chrome / Firefox 桌面版:非活跃标签中,
setTimeout(0)实际延迟 ≥1000ms - Firefox 追踪脚本识别开启后:后台标签中延迟直接拉到 ≥10000ms(10 秒),且从文档加载完 30 秒后开始生效
- 页面正在播放 Web Audio(哪怕静音)可绕过该限制,但需显式调用
audioContext.resume()
想绕过 4ms 限制?基本没戏,但可以换思路
不要试图“欺骗”浏览器层级计数(比如用 postMessage + message 事件模拟零延迟),现代浏览器已将这类旁路也纳入统一节流范围。
- 真正需要亚毫秒调度的场景(如音频采样、高频动画帧同步),应该用
requestAnimationFrame或 WebAssembly +Atomics.wait(仅 Worker 中可用) - 若只是想让逻辑尽快脱出当前执行栈,
queueMicrotask是更轻量、更可控的选择,它不计入宏任务层级,也不受 4ms 限制 - 用
setTimeout(fn, 0)做“防阻塞”时,别依赖精确时序——它只保证“下一宏任务开始后执行”,不保证“下一帧前”或“1ms 内”
setTimeout(0),只要它是从上一个 setTimeout 回调里发起的,层级就在累加。











