将零延迟setTimeout放在函数开头代码执行更快的原因是什么?
两种场景的核心差异先明确
我们先把两种写法的核心逻辑抽离出来,方便对比:
写法1(慢版本:计算完成后调度下一次)
function count() { // 先执行100万次运算 do { i++ } while (i % 1e6 != 0) if (i < 1e7) { // 运算完成后才发起下一次定时器调度 setTimeout(count, 0) } }
写法2(快版本:进入函数先调度下一次)
function count() { // 进入函数第一时间就发起下一次定时器调度 if (i < 1e7) { setTimeout(count, 0) } // 再执行100万次运算 do { i++ } while (i % 1e6 != 0) }
耗时差异的核心原因:浏览器的setTimeout嵌套延迟规则
HTML标准明确规定:当setTimeout的嵌套调用层数≥5时,若设置的延迟小于4ms,浏览器会强制将延迟调整为4ms。这个规则的初衷是避免过密的定时器调度挤占主线程资源,导致页面交互卡顿。
两种写法的调度链路差异
慢版本的调度链路:触发嵌套延迟,等待时间串行累加
每一次setTimeout调用,都是在上一次定时器触发的count函数执行末尾才发起的,形成了严格的嵌套调用链:
- 初始调用
count()→计算完成→发起第1次setTimeout(count,0) - 第1次定时器触发执行
count→计算完成→发起第2次setTimeout(count,0) - 以此类推...
当调用到第6次setTimeout时,嵌套层数已经达到阈值,后续每一次调度都会被强制加上4ms的额外延迟。整个流程需要调度9次(10批计算,最后一批不需要调度),后面5次调度总共会产生至少20ms的额外延迟,且延迟和计算时间是串行累加的,整体耗时就会变长。
快版本的调度链路:等待时间与计算时间重叠,无额外损耗
进入count函数的第一时间就发起下一次setTimeout调度,此时当前批次的计算还没开始:
- 就算触发了4ms的嵌套延迟,这个等待时间也会和当前批次的计算时间(单批100万次运算通常耗时5-10ms)重叠,等当前计算完成时,下一次定时器的等待时间已经跑完,回调可以直接执行
- 不会出现慢版本中“计算完成→等4ms→再计算下一批”的串行等待场景,所有额外等待时间都被计算过程覆盖,整体执行速度自然快很多。
内容的提问来源于stack exchange,提问作者Nikita Vlasenko
相关产品推荐
相关产品推荐

