渲染流水线深度拆解:CPU、GPU、合成器与显示器的协同工作

很多同学问我:「为什么我的页面 DOM 操作不多,GPU 也闲着,但滚动就是掉帧?」

答案往往藏在这条流水线里。CPU、GPU、合成器、显示器——这四个角色只要有一个掉链子,卡顿就来了。我调过不少性能问题,说句实话,大部分开发者只盯着 JavaScript 执行时间,却忽略了整条链路的配合。

一、四阶段流水线全景

先看整体流程。一个帧从诞生到显示在屏幕上,要经历四个阶段:

  1. CPU 阶段:解析 HTML/CSS、执行 JavaScript、计算样式、构建布局树。
  2. GPU 阶段:将 CPU 生成的绘制指令光栅化,生成位图纹理。
  3. 合成器阶段:将多个图层(Layer)合成为一张最终图像。
  4. 显示器阶段:按照固定刷新率(60Hz/120Hz)从缓冲区读取并显示。

说白了,这就是一条「数据→像素→屏幕」的生产线。任何一个环节超时,帧就丢了。

核心认知:60fps 意味着每帧只有 16.67ms。CPU、GPU、合成器加起来必须在这个时间内完成所有工作。超过 16.67ms,下一帧就会被跳过。

渲染流水线四阶段协同图 渲染流水线四阶段协同工作 CPU 阶段 JS 执行 / 样式计算 布局 / 分层 / 绘制指令 GPU 阶段 光栅化 生成位图纹理 合成器阶段 图层合成 提交到帧缓冲区 显示器 刷新显示 ⏱ 每帧总预算:16.67ms(60Hz) CPU + GPU + 合成器 必须在预算内完成,否则丢帧

二、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 合成器的工作流程

  1. 接收 GPU 输出的各图层位图纹理
  2. 根据图层树的 z-index 和 transform 信息计算合成顺序
  3. 将图层合并到帧缓冲区(Frame Buffer)
  4. 通知显示器:新帧已就绪

这里有个关键点:合成器在 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、合成器、显示器,每个环节都有明确的性能指标和优化手段。掌握了这条链路的协同原理,你就有了定位卡顿的「地图」和「指南针」。