不同浏览器处理Macrotask队列与渲染的差异问题咨询
我原本认为各浏览器对JavaScript Event Loop处理宏任务(Macrotask)队列的逻辑基本一致,简化流程(暂不考虑脚本评估、微任务队列等)为:
- 从宏任务队列取出最旧的任务执行
- 进行渲染
- 重复上述步骤
但实际测试发现Chrome表现存在不一致,Firefox则基本遵循上述流程。
测试示例
代码实现
index.html
<head> <script src="index.js" defer></script> </head>
index.js
"use strict"; function sleep(ms) { const start = performance.now(); while ((performance.now() - start) < ms); } function doNextPart() { currentPart++; sleep(20); addToRender(currentPart.toString()); } function addToRender(part) { partsList.push(part); } function render(timestamp) { frame++; console.log(`frame: ${frame} \t rendering time: ${timestamp}`); for (const part of partsList) { const itemDiv = document.createElement("div"); itemDiv.innerHTML = `part: ${part} || frame: ${frame}, ${timestamp}`; partsContainerDiv.appendChild(itemDiv); } document.body.appendChild(partsContainerDiv); partsList.length = 0; if (frame <= 5) { window.requestAnimationFrame(render); } } const partsContainerDiv = document.createElement("div"); const partsList = []; let frame = 0; let currentPart = 0; window.requestAnimationFrame(render); setTimeout(doNextPart); setTimeout(doNextPart); setTimeout(doNextPart);
代码说明
- 使用
requestAnimationFrame()渲染partsList数组内容,渲染后清空数组,同时记录当前frame数与timestamp doNextPart()通过sleep()阻塞Event Loop指定时长,再将currentPart加入partsList待渲染- 渲染循环在5帧后停止
- 三次调用
setTimeout(doNextPart)将任务加入宏任务队列 - 用
console.log()记录全部5帧信息,重点查看无宏任务时的最后几帧时间戳
预期结果
- 取出第一个宏任务,执行
doNextPart(),将"1"加入partsList - 执行渲染,调用
render(),HTML输出part: 1 || frame: 1 - 重复上述流程
浏览器测试结果
Firefox 110.0.1
最终HTML输出符合预期:
part: 1 || frame: 1 part: 2 || frame: 2 part: 3 || frame: 3
- 前3帧每帧耗时约25ms(包含
sleep(20ms)),后2帧每帧约8ms,共5帧 - Firefox会在宏任务之间触发渲染,尝试匹配显示器刷新率(如120Hz约每8ms渲染一次)
Chrome 110.0.5481.178
HTML输出如下:
part: 1 || frame: 2 part: 2 || frame: 2 part: 3 || frame: 2
- 三个宏任务的结果在同一帧渲染,该帧耗时约91.7ms
- 性能面板显示任务执行正常,但渲染仅在最后一个任务完成后触发
- 当
sleep()时长增大到100ms时,Chrome会符合预期地在宏任务间进行渲染
问题解答
1. 我的实现是否存在严重错误?
没有严重错误。代码逻辑清晰,通过阻塞型sleep模拟长耗时宏任务,用requestAnimationFrame追踪渲染时机,能够准确复现浏览器在Event Loop处理上的差异。唯一需要注意的是:requestAnimationFrame的回调触发时机是下一帧渲染前,而非宏任务执行后立刻触发,但这不影响对核心差异的测试。
2. 该行为属于正常表现还是bug?
属于正常表现。浏览器在Event Loop的宏任务与渲染调度上,标准只规定了大致流程,并没有强制要求每次宏任务执行后必须立刻触发渲染。Chrome的调度逻辑包含渲染合并优化:当连续宏任务的总耗时未超过某个阈值(比如一帧的预期时长)时,会将渲染操作延后,避免频繁渲染带来的性能开销;而当单个宏任务耗时足够长(比如100ms),超过一帧时长时,Chrome会触发渲染以避免页面长时间无响应。Firefox则更倾向于在每个宏任务后尝试触发渲染,尽可能保持渲染的及时性。
3. 各浏览器处理Macrotask队列的表现是否应保持一致?
从标准层面来说,不需要完全一致。HTML和ES标准对Event Loop的调度只定义了核心规则,比如宏任务队列的执行顺序、微任务的处理时机,但在渲染时机的具体调度策略上,留给了浏览器厂商一定的优化空间。不同浏览器会根据自身的性能目标、用户体验需求制定不同的调度逻辑,因此出现行为差异是合理的。开发者编写代码时,不应依赖某一种浏览器的特定调度行为,而应遵循标准规范,确保代码在不同浏览器下的兼容性。
内容的提问来源于stack exchange,提问作者NaR_NotARobot

