渲染流水线深度拆解:CPU、GPU、合成器与显示器的协同工作
很多同学问我:「为什么我的页面 DOM 操作不多,GPU 也闲着,但滚动就是掉帧?」
答案往往藏在这条流水线里。CPU、GPU、合成器、显示器——这四个角色只要有一个掉链子,卡顿就来了。我调过不少性能问题,说句实话,大部分开发者只盯着 JavaScript 执行时间,却忽略了整条链路的配合。
一、四阶段流水线全景
先看整体流程。一个帧从诞生到显示在屏幕上,要经历四个阶段:
- CPU 阶段:解析 HTML/CSS、执行 JavaScript、计算样式、构建布局树。
- GPU 阶段:将 CPU 生成的绘制指令光栅化,生成位图纹理。
- 合成器阶段:将多个图层(Layer)合成为一张最终图像。
- 显示器阶段:按照固定刷新率(60Hz/120Hz)从缓冲区读取并显示。
说白了,这就是一条「数据→像素→屏幕」的生产线。任何一个环节超时,帧就丢了。
核心认知:60fps 意味着每帧只有 16.67ms。CPU、GPU、合成器加起来必须在这个时间内完成所有工作。超过 16.67ms,下一帧就会被跳过。
二、CPU 阶段:从 DOM 到绘制指令
CPU 是整条流水线的起点。它负责把开发者写的代码变成 GPU 能理解的「绘制指令」。
2.1 子阶段拆解
| 子阶段 | 耗时占比 | 常见瓶颈 |
|---|---|---|
| JavaScript 执行 | 30% - 50% | 长任务、强制回流、大量 DOM 操作 |
| 样式计算 | 10% - 20% | 复杂选择器、大量 CSS 变量 |
| 布局 | 15% - 25% | 大量元素、表格布局、flex 嵌套 |
| 分层 | 5% - 10% | 不必要的层、层爆炸 |
| 绘制指令生成 | 10% - 15% | 复杂绘制路径、阴影、渐变 |
我遇到过最典型的案例:一个后台表格页面,每次排序都要重新计算全部 5000 行的样式。Chrome DevTools 的 Performance 面板一看,样式计算占了 80ms。嗯,这就是典型的「CPU 过载」。
我的习惯:每次写代码前,先问自己「这个操作会触发 Layout 吗?」。如果会,尽量用 transform 和 opacity 替代。这两个属性只触发合成,不触发布局和绘制。
2.2 关键指标:帧预算分配
60fps 下,CPU 阶段最好控制在 8ms 以内。为什么?因为 GPU 和合成器还要分剩下的时间。我一般这样分配:
- JS 执行:≤ 4ms
- 样式计算 + 布局:≤ 2ms
- 绘制指令生成:≤ 2ms
超过这个阈值,就要考虑优化了。我曾经在一个大屏可视化项目里,把 JS 执行时间从 12ms 压到 3ms,靠的就是把数据预处理放到 Web Worker 里。
三、GPU 阶段:光栅化的秘密
CPU 生成绘制指令后,这些指令被发送到 GPU。GPU 负责「光栅化」——把矢量指令变成像素点阵。
这里有个很多人忽略的点:GPU 不是万能的。它擅长并行处理大量简单计算,但遇到复杂路径、大量阴影、超大纹理时,一样会卡。
避坑指南:我曾经在移动端用了一个 4000×4000 像素的渐变背景图作为页面背景。结果 GPU 光栅化花了 25ms,直接导致页面滚动掉帧。解决方案很简单:把背景切成 4 块小图,或者用 CSS 渐变替代。
3.1 GPU 性能瓶颈清单
| 场景 | 原因 | 优化方向 |
|---|---|---|
| 超大纹理 | 显存带宽不足 | 压缩图片、使用 WebP/AVIF |
| 复杂 SVG 路径 | 光栅化计算量大 | 简化路径、使用 Canvas 替代 |
| 大量阴影/模糊 | 需要多次采样 | 减少阴影层级、使用 box-shadow 替代 filter |
| 频繁重绘 | GPU 上下文切换 | 使用 will-change 提升为合成层 |
四、合成器阶段:图层合并的艺术
GPU 光栅化完成后,合成器登场。它的工作是把多个图层(Layer)合并成一张最终图像。
你想想看,为什么 transform 和 opacity 动画性能好?因为这两个属性只影响合成阶段,不需要重新走 CPU 的布局和绘制。合成器只需要把图层做位移或透明度变化,然后合并。
4.1 图层创建条件
浏览器什么时候会创建独立图层?我总结了几种常见情况:
- 有
will-change: transform或will-change: opacity - 使用了
transform或opacity动画 - 有
position: fixed或position: sticky - 有
overflow: scroll且内容可滚动 - 有
video、canvas、iframe等独立渲染元素
注意:图层不是越多越好。每个图层都会占用 GPU 内存。我见过一个页面创建了 200 多个图层,结果 GPU 内存爆了,直接白屏。合理控制图层数量,通常 10-20 个以内比较安全。
4.2 合成器的工作流程
- 接收 GPU 输出的各图层位图纹理
- 根据图层树的 z-index 和 transform 信息计算合成顺序
- 将图层合并到帧缓冲区(Frame Buffer)
- 通知显示器:新帧已就绪
这里有个关键点:合成器在 GPU 上运行。所以它和 GPU 光栅化共享同一块硬件资源。如果 GPU 光栅化已经占满了,合成器就得排队等。
五、显示器阶段:最后的 16.67ms
显示器按照固定频率从帧缓冲区读取数据。60Hz 就是每 16.67ms 读一次。120Hz 就是每 8.33ms 读一次。
如果合成器没能在显示器读取之前把新帧提交到缓冲区,显示器就会重复显示上一帧。这就是「丢帧」。
5.1 VSync 信号的作用
显示器通过 VSync(垂直同步)信号告诉 GPU:「我准备好读下一帧了,你赶紧把数据放好。」
浏览器会监听 VSync 信号来触发渲染。这就是为什么 requestAnimationFrame 的回调频率和显示器刷新率一致——它就是在 VSync 信号到来前被调用的。
我的经验:用 requestAnimationFrame 做动画时,回调里不要做超过 5ms 的同步操作。否则会错过 VSync 窗口,导致下一帧延迟。如果计算量大,拆成多个 rAF 帧来做。
六、整条链路的瓶颈诊断
当页面卡顿时,怎么快速定位是哪个环节出了问题?我一般用 Chrome DevTools 的 Performance 面板,看这几个关键指标:
| 现象 | 可能瓶颈 | 验证方法 |
|---|---|---|
| JS 执行时间 > 10ms | CPU 过载 | 看 Main 线程的 Task 时长 |
| 样式计算 + 布局 > 5ms | 强制回流 / 复杂布局 | 看 Layout 和 Recalculate Style 事件 |
| GPU 光栅化 > 8ms | 纹理过大 / 复杂绘制 | 看 Rasterizer 线程和 GPU 内存 |
| 合成器延迟 | 图层过多 / 合成器排队 | 看 Compositor 线程的帧时间 |
| 显示器丢帧 | 帧缓冲区未及时更新 | 看 FPS 计数器和帧时间线 |
说白了,性能调优就是「找到最慢的那个环节,把它优化到预算以内」。CPU 慢了优化 JS,GPU 慢了减少绘制,合成器慢了控制图层。
总结一句话:渲染流水线不是黑盒。CPU、GPU、合成器、显示器,每个环节都有明确的性能指标和优化手段。掌握了这条链路的协同原理,你就有了定位卡顿的「地图」和「指南针」。