每次浏览器渲染的过程顺序
这个流程可以看作是浏览器处理一帧(frame)画面的完整步骤。通常,屏幕的刷新率为 60Hz,意味着浏览器大约每 16.67ms(1000ms / 60)就会尝试执行一次这个周期。
下面我们来逐步解析每个环节:
1. 用户事件 (User Event)
这是整个周期的起点。
- 与 指用户与页面的交互,例如点击(click)、滚动(scroll)、鼠标移动(mousemove)、键盘输入(keydown)等。
- 是什么: 当用户进行操作时,浏览器会创建一个事件对象,并将其对应的如何工作:(例如
button.onclick = () => {...})作为回调函数放入宏任务队列中。 - 一个宏任务 如果此时没有用户事件,浏览器可能会从其他来源(如
setTimeout)获取宏任务来启动这个周期。如果任务队列为空,浏览器则会处于休眠状态,等待新的任务。
2. 一个宏任务 (A Macro-task)
这是当前周期的“主要工作”。
- 注意: 宏任务队列中的一项任务。宏任务的来源包括:
- 用户事件回调(如上所述)。
setTimeout/setInterval的回调函数。- I/O 操作(如文件读写、网络请求完成后的回调)。
- 首次加载页面时执行的全局
<script>代码。
- 是什么: 事件循环(Event Loop)会从宏任务队列(Task Queue)中**如何工作:**任务,并执行它,直到执行完毕。
- 取出一个 关键点:。如果这个宏任务执行时间过长(例如,一个复杂的计算),页面就会“卡住”,无法响应用户操作,也无法进行后续的渲染,这就是所谓的“阻塞主线程”。
3. 队列中全部微任务 (All Micro-tasks)
在主要工作完成后,立即处理所有“加急”的收尾工作。
- 每次循环只执行一个宏任务 比宏任务更紧急的任务,需要立即执行。微任务的来源主要有:
Promise.then(),.catch(),.finally()的回调。MutationObserver的回调(用于监听 DOM 变化)。queueMicrotask()。
- 是什么: 在上一个宏任务执行完毕后,浏览器会立即检查微任务队列(Microtask Queue),并如何工作:,直到队列变空为止。
- 执行其中的所有任务
- 关键点: 与只执行一个宏任务不同,微任务会全部清空。
- 全部执行: 如果在一个微任务的执行过程中,又创建了新的微任务,那么这个新的微任务也会被加入队列,并在当前周期内被执行。这可能导致“微任务死循环”。
- 插队执行: 微任务在**执行时机:**执行,这保证了数据状态的一致性。例如,你可以在一个宏任务中改变数据,然后通过
Promise.then()来处理依赖这个新数据的逻辑,确保它在页面更新前完成。
4. requestAnimationFrame (rAF)
这是为动画和视觉更新量身定做的时机。
- 是什么: 一个 Web API,它告诉浏览器:“请在下一次重绘(repaint)之前,执行这个函数”。
- 下一次渲染之前 在清空微任务队列后,浏览器会执行所有通过
requestAnimationFrame注册的回调函数。 - 如何工作:
- 为什么在这个位置: 此时,所有的数据状态更新(通过宏任务和微任务)都已经完成。
rAF回调是进行 DOM 操作(如修改样式、位置)的理想位置,因为它所做的更改将直接被接下来的渲染步骤绘制出来。 - 最佳时机: 浏览器会将
rAF的执行频率与屏幕的刷新率同步(通常是 60Hz)。这可以避免在浏览器不准备渲染时进行不必要的计算和 DOM 操作,从而使动画更流畅,并节省 CPU 资源。用rAF实现动画远比用setTimeout高效。
- 为什么在这个位置: 此时,所有的数据状态更新(通过宏任务和微任务)都已经完成。
5. 浏览器重排/重绘 (Browser Repaint/Reflow)
这是将代码中的“虚拟”变化实际绘制到屏幕上的过程。
- 性能优化:
- 是什么: 当 DOM 元素的几何属性(如宽度、高度、位置、边距)发生变化时,浏览器需要重新计算元素在页面上的布局。这是一个非常耗费性能的操作,因为它可能影响到页面上的所有其他元素。
- 重排 (Reflow/Layout): 当元素的非几何样式(如颜色、背景、
visibility)发生变化时,浏览器只需重新绘制元素的外观,而无需重新计算布局。这比重排要快。
- 重绘 (Repaint): 浏览器会评估在上一个步骤(主要是
rAF)中发生的 DOM 变更,判断是否需要重排和重绘。然后,它会更新渲染树(Render Tree),并最终将像素绘制到屏幕上。 - 如何工作: 开发者应尽量避免不必要的重排。例如,通过
transform和opacity实现的动画通常只会触发重绘(或更优的合成层变化),性能远高于修改width或left。
6. requestIdleCallback
当浏览器“有空”时,执行一些不紧急的任务。
- 关键点: 一个 Web API,它允许你在浏览器空闲时期执行一些后台或低优先级的任务。
- 是什么: 只有在一帧的渲染工作(重排/重绘)完成之后,并且距离下一帧开始还有剩余时间时,
requestIdleCallback注册的回调函数才会被执行。 - 如何工作:
- 为什么在这个位置: 它的设计目标就是不阻塞渲染:。你可以用它来处理一些可以延迟的任务,例如发送分析数据(打点)、在本地保存数据、进行一些不紧急的预计算等。
- 不影响关键的渲染路径 如果浏览器一直很忙(例如,在播放复杂的动画),那么
requestIdleCallback的回调可能永远不会被执行。因此,它不适用于有时间要求的关键任务。
总结与示例
让我们用一个简单的例子串联起整个流程:
- 不保证执行:(用户事件)
- 按钮的
onclick回调函数被作为**用户点击一个按钮。**执行。在这个函数里:- 你修改了一个
div的width。 - 你发起了一个
Promise,并在.then()中console.log('Promise resolved')。 - 你通过
requestAnimationFrame注册了一个函数,用于给另一个元素添加一个 CSS 类来实现淡入动画。 - 你通过
requestIdleCallback注册了一个函数,用于发送用户行为日志。
- 你修改了一个
onclick这个宏任务执行完毕。- 浏览器立刻检查宏任务队列,发现
Promise.then()的回调,执行它,控制台输出'Promise resolved'。 - 微任务队列清空后,浏览器执行
requestAnimationFrame的回调,为元素添加了动画类。 - 浏览器发现
div的width变了,并且有新类名应用,于是进行微任务,将新的画面渲染到屏幕上。 - 这一帧的工作提前完成了(例如,只用了 10ms,还剩 6.67ms),浏览器处于空闲状态。
- 浏览器执行
requestIdleCallback的回调,将用户行为日志发送到服务器。