8.3 渲染流水线
渲染流水线是浏览器将 DOM、CSSOM 转化为屏幕可视化像素的完整执行流程,与事件循环深度协同,按固定帧率周期性执行。完整的渲染流水线分为样式计算、布局、绘制、合成四个核心阶段,不同阶段对应不同的性能开销。配合requestAnimationFrame精准控制渲染时机、requestIdleCallback利用空闲时间调度低优先级任务,可以实现渲染性能的精细化管控。
8.3.1 渲染帧的完整流程:样式 → 布局 → 绘制 → 合成
浏览器以固定帧率(通常 60fps,即每 16.67ms 一帧)周期性执行渲染流水线。每一帧都会根据最新的 DOM 与样式状态,完成从样式计算到屏幕合成的全流程,最终更新画面。并非所有场景都会走完所有阶段,根据变更的属性不同,流水线会提前终止,对应不同的性能开销。
一、样式计算(Style / Recalculate Style)
- 核心工作:根据 CSS 样式表、类名变更、属性修改,重新计算所有受影响元素的最终计算样式(computed style)。
- 触发场景:任何 CSS 属性、class、id 等影响样式的修改。
- 开销程度:低,仅做样式匹配与计算,不涉及几何信息。
- 说明:这是渲染流水线的第一步,所有视觉变更都会先经过样式计算,再决定后续流程。
二、布局(Layout / Reflow)
- 核心工作:根据元素的样式与内容,计算元素的几何信息(位置、尺寸、排版、层级关系),生成渲染树的布局结构。
- 触发场景:修改 width、height、margin、top、font-size 等布局属性,DOM 节点增删、内容变化等。
- 开销程度:极高,需要递归计算元素间的位置影响,是渲染性能的核心瓶颈。
- 分类:全局布局(影响整个文档)、局部布局(仅影响部分区域,开销更低)。
- 对应概念:这一步就是常说的「重排(回流)」,重排后必然触发后续的绘制阶段。
三、绘制(Paint / Repaint)
- 核心工作:根据布局结果与样式,在对应图层上填充像素,包括颜色、背景、边框、阴影、文字、图片等所有视觉像素信息。
- 触发场景:修改 color、background、box-shadow、visibility 等不影响布局的视觉属性。
- 开销程度:中等,像素级填充,大面积绘制开销会显著上升。
- 对应概念:这一步就是「重绘」,仅影响视觉像素,不改变元素位置与尺寸。
- 图层特性:浏览器会将页面分为多个图层分别绘制,减少全页面重绘的开销。
四、合成(Composite)
- 核心工作:将所有绘制好的图层,按照正确的层级顺序、透明度、变换关系,由 GPU 混合合成为最终的屏幕画面。
- 触发场景:修改 transform、opacity 等仅影响图层合成的属性。
- 开销程度:极低,由 GPU 并行处理,无需 CPU 重新计算布局与像素,是性能最优的渲染阶段。
- 核心价值:硬件加速动画的本质,就是将动画控制在合成阶段,全程不触发布局与绘制。
各阶段对比总结
阶段 | 核心操作 | 触发属性示例 | 性能开销 | 跳过条件 |
样式计算 | 计算元素最终样式 | class、color、width | 低 | 无样式变更时跳过 |
布局 | 计算几何位置尺寸 | width、margin、top、font-size | 极高 | 无布局属性变更时跳过 |
绘制 | 填充视觉像素 | color、background、shadow | 中等 | 无视觉属性变更时跳过 |
合成 | 图层混合输出 | transform、opacity | 极低 | 无图层变更时跳过 |
渲染优化的核心思路:尽可能让变更停留在更靠后的阶段,优先使用合成属性,避免触发布局与全量重绘。
8.3.2 requestAnimationFrame 与渲染时机
一、核心定位
requestAnimationFrame(简称 rAF)是浏览器专门为动画与渲染优化提供的原生 API,回调函数会在每帧渲染流水线开始前自动执行,与屏幕刷新率严格同步,是 JS 驱动动画的标准最佳实践。
二、执行时机
在事件循环的完整流程中,rAF 的执行节点位于:宏任务执行完毕→微任务队列清空→ requestAnimationFrame回调执行→正式执行渲染流水线
核心特点:rAF 回调恰好发生在渲染之前,此时修改的样式与布局会被当前帧的流水线一次性处理,不会出现样式与渲染不同步的掉帧问题。
三、与定时器动画的核心区别
对比维度 | setInterval / setTimeout 动画 | requestAnimationFrame 动画 |
执行时机 | 作为宏任务调度,与渲染时机不同步 | 渲染前固定执行,与帧率严格同步 |
帧率稳定性 | 容易掉帧、卡顿,回调可能落在两帧之间 | 自动匹配屏幕刷新率,流畅稳定 |
页面可见性 | 页面隐藏后依然执行,浪费性能 | 页面隐藏后自动暂停,切回后恢复 |
时间精度 | 受事件循环排队影响,时间偏差大 | 传入高精度时间戳,进度计算精准 |
四、基础用法
function animate(timestamp) { // timestamp 为高精度时间戳,单位毫秒 // 更新动画状态 element.style.transform = `translateX(${timestamp / 10}px)`; // 递归调用,持续下一帧 requestAnimationFrame(animate); } // 启动动画 requestAnimationFrame(animate);五、典型应用场景
- JS 驱动的逐帧动画、Canvas 动画、游戏渲染
- 精确控制 DOM 动画的渲染时机,避免掉帧
- 批量 DOM 修改统一在渲染前执行,减少重排重绘次数
8.3.3 空闲调度:requestIdleCallback
一、核心定位
requestIdleCallback(简称 rIC)是浏览器的空闲时间调度 API,回调会在每帧渲染完成后、页面处于空闲状态时执行。如果当前帧还有剩余时间(距离下一帧开始还有空闲),就执行空闲任务;如果帧时间已用完,则推迟到下一帧空闲时再执行,完全不影响关键渲染与交互。
二、执行时机
完整的帧周期执行顺序:宏任务→微任务→ requestAnimationFrame →渲染流水线→ requestIdleCallback(空闲时)
核心特点:优先级最低,仅利用帧的剩余时间执行,不会阻塞主线程、不会影响页面渲染与交互响应。
三、核心特性
空闲时间参数:回调接收deadline对象,包含:
deadline.timeRemaining():返回当前剩余空闲时间(毫秒),任务执行前判断,避免超时。deadline.didTimeout:是否因为超时强制执行。
超时参数timeout:第二个参数可设置超时时间,超过该时间即使没有空闲也会强制执行,避免任务无限推迟。
requestIdleCallback(doIdleWork, { timeout: 2000 });
四、基础示例
function doIdleWork(deadline) { // 还有剩余时间,且有任务待处理 while (deadline.timeRemaining() > 0 && tasks.length > 0) { processTask(tasks.shift()); } // 还有任务,继续申请下一次空闲 if (tasks.length > 0) { requestIdleCallback(doIdleWork); } } requestIdleCallback(doIdleWork, { timeout: 3000 });五、典型应用场景
- 非紧急的后台任务:埋点数据上报、日志上报
- 预加载与预计算:提前加载后续页面资源、大数据预计算
- 低优先级计算:大批量数据处理、索引构建,拆分到多个空闲帧执行
- 非关键 DOM 操作:次要元素的延迟渲染、非核心内容的更新
六、注意事项
- 不保证执行时机,不要用于关键业务逻辑与动画。
- 单次任务不要占用过长时间,建议拆分小任务分批执行,避免占用空闲时间过长。
- 低版本浏览器需要 polyfill 兼容。