settimeout(fn, 0) 并非立即执行,而是注册宏任务,需等同步代码和微任务执行完毕后才运行,且受浏览器最小延迟4ms限制及主线程阻塞影响。

因为 setTimeout(fn, 0) 本质不是“立即执行”,而是“立刻注册一个宏任务”,它必须等当前同步代码跑完、所有微任务清空后,才轮到它执行——这是事件循环的硬性规则,不是实现缺陷。
它被设计成宏任务,不是 bug,是规范
JavaScript 主线程单线程运行,所有代码按调用栈顺序执行。如果允许 setTimeout(fn, 0) 插入当前执行流,就等于允许异步逻辑打断同步逻辑,会破坏执行一致性。所以 HTML 标准强制规定:所有 setTimeout 回调,无论 delay 多小,都必须作为宏任务进入任务队列末尾。
- 浏览器遇到
setTimeout(fn, 0),立刻触发“计时器初始化”,把fn封装为新宏任务,压入宏任务队列 - 它不会进调用栈,也不会中断
for循环或递归 - 哪怕主线程此刻空闲,它也得排队,不能越过正在执行的同步代码
实际延迟不止来自排队,还有浏览器限制
即使宏任务队列为空,setTimeout(fn, 0) 也不会在 0ms 执行——因为浏览器有最小延迟兜底机制。
- HTML Standard 明确规定:定时器最小延迟不得小于 4ms
- 这是为了防止高频调度导致 CPU 过载、功耗升高,属于主动性能调控
- 也就是说,设成 0,实际至少等 4ms;设成 1ms 或 3ms,结果也一样是 ≥4ms
微任务永远比它快,这是执行顺序决定的
事件循环每轮只处理一个宏任务,但会清空全部微任务。这就造成 Promise.then() 总比 setTimeout(fn, 0) 先执行。
- 同步代码 → 所有微任务(如 Promise 回调)→ 渲染 → 下一个宏任务(含你的 setTimeout)
- 所以
console.log(1); Promise.resolve().then(() => console.log(2)); setTimeout(() => console.log(3), 0); console.log(4);输出一定是 1 → 4 → 2 → 3 - 这不是 setTimeout “慢”,而是它本来就排在微任务之后
主线程卡住,它就只能干等
宏任务必须等主线程真正空闲才能执行。如果前面有长耗时同步操作,它的“0 毫秒”就彻底失效。
- 比如一个持续 2 秒的
while循环,期间 timer 到期,回调进队列,但要等到循环结束、同步代码跑完才开始处理队列 - 这时实际延迟是 2 秒 + 微任务耗时 + 调度开销,和设置的 0 毫秒毫无关系
- 页面切到后台时,浏览器还会进一步节流,延迟可能达数秒











