正在刊行长文 · Essay
2026-08-30所有内容
随机比特 · Random Bits

Promise 明明异步,页面为什么仍要等它跑完

2026-08-30AI Engineering / Systemsrbits.uk
Promise 明明异步,页面为什么仍要等它跑完

我们把一段确定性计算拆成 120,000 个 Promise 回调。5 次实验里,下一次 requestAnimationFrame 回调和预先排好的计时器,都等到 120,000 个单元全部执行完成后才运行。

代码确实“异步”了,页面却没有在中途获得一次插入机会。异步只说明回调不在当前调用栈里立刻执行,不保证浏览器会在微任务之间获得一次渲染或处理其他 task 的机会。

Promise 回调仍在主线程中执行

需要先区分两种执行语义。new Promise(executor) 会同步调用 executor;这里聚焦 .then(...) reaction。它不会转移到另一条线程,只是离开当前调用栈,排到稍后执行。

实验中的自补充链可以缩成这段代码:

function step() {
  workOneUnit();
  if (++done < 120000) Promise.resolve().then(step);
}
Promise.resolve().then(step);

ECMAScript 先把 Promise reaction 包装成 Job,再交给宿主入队;在浏览器 Window 中,WHATWG HTML 把它接入微任务队列。Promise 把回调推迟到微任务队列,却没有承诺在它之前或之间给页面一次渲染机会。

因此,这段代码和同步递归的调用栈形状不同,却仍在同一条主线程上消耗 CPU。判断它是否为页面保留响应机会,需要观察事件循环何时能离开当前检查点,不能只看是否使用了 Promise。

微任务队列会在渲染前持续清空

一个 task 结束后,浏览器会执行微任务检查点。只要队列不空,就取出最早的微任务运行。上面的 step 每完成一次又补入下一次,所以队列直到固定上限跑完才真正为空。

Chrome 151 自补充 Promise 微任务时间轨:5 次均在 120,000 个单元完成后才让 rAF 与计时器获得回调机会

在 Chrome 151、V8 15.1 的可见独立页面实验中,自补充 Promise 链的三项中位数分别是工作 50.7 ms、首个 rAF 回调 51.0 ms、计时器回调 50.9 ms。更重要的是顺序:5/5 次里,两种回调看到的完成数都是 120,000。

每次试验都先在一个 rAF 回调后对齐起点,再同时安排“下一次 rAF”与一个计时器标记,随后启动工作。记录直接给出工作完成相对两种回调在同一轮试验里的先后。

rAF 在渲染更新流程中回调,但发生在实际绘制之前,所以这里只能把它当作一次渲染更新机会的代理,不能说像素已经上屏。短小 Promise 链也会先于 rAF 清空:500 单元基线的中位工作耗时只有 0.8 ms。这说明风险来自同一检查点里的总工作量,而非 Promise API 本身。

任务边界以总耗时换取响应机会

我们保持相同的 120,000 个计算单元和相同累加结果,只改成每 4,000 个单元结束当前 task,再用 setTimeout 安排下一批。在同一 Chrome 151 对照里,5/5 次 rAF 都在全部工作完成前运行,中位首个 rAF 回调为 6.3 ms,中位总工作耗时为 752.8 ms。

等量工作对照显示连续微任务在完成前触发 rAF 为 0 比 5,计时器分批为 5 比 5,但总耗时更长

分批没有让计算更快;本次结果用更长总耗时换来了其他工作获得机会。分批需要反复排入计时器 task,HTML 对深层嵌套计时器规定了至少 4 ms 的下限,观测值还包含本机调度与页面状态的影响。实验没有拆分这些因素各自的贡献,这个耗时差距不能外推成固定损失比例。setTimeout(0) 不保证零延迟,任务边界也不保证每批之间都有一帧;它只是本实验里最容易观察的对照手段。

两个长工作组逐轮得到相同 accumulator,说明计算内容和数量一致。预排计时器也在分批组的 5/5 次试验里早于全部工作完成;但这仍不能推出每个批次之间都会执行一次计时器或渲染。

验收一段异步计算是否实际让出调度机会,可以同时记录工作完成时间、下一次 rAF 回调、预排计时器回调,以及两种回调发生时已完成的单元数。再引入明确的 task 边界,以相同工作量重跑。若回调只能看到“全部完成”,异步语法没有带来中途响应机会。

本次三组各 5 次实验只覆盖这台机器、可见 Chrome 页面和固定参数;它不代表其他浏览器或 Node.js,也没有比较 Worker、scheduler.yield() 等生产方案。机制依据见 WHATWG HTML 的 事件循环微任务检查点,以及 ECMAScript 2026 的 Promise reaction

随机比特公众号二维码
公众号 · 随机比特
从 AI 工具热闹里拆工程真相

写边界、控制面、上下文、成本与安全。