3、渲染流水线(下):GPU阶段——从布局到光栅化与合成

上一章我们聊完了 CPU 阶段,也就是样式计算和布局。说白了,那部分是在算「每个元素该长什么样、该放哪儿」。但真正把像素画到屏幕上的活儿,得交给 GPU。

这一章,我们就顺着流水线往下走,看看 GPU 到底干了什么。我个人习惯把 GPU 阶段拆成三个关键步骤:分层、光栅化、合成。每一步都可能成为卡顿的元凶。

3.1 分层:为什么浏览器要「拆」页面?

你想想看,一个网页可能有几百个元素。如果每次滚动或动画都要重绘所有像素,那性能肯定崩了。所以浏览器想了个办法——分层。

它会根据一些规则,把页面拆成多个独立的「层」(Layer)。比如一个固定定位的导航栏,单独放一层;一个正在播放动画的 div,也单独放一层。这样,只有变化的层需要重绘,其他层直接复用之前的缓存。

核心原则: 层越多,合成越快,但内存占用越高。这是一个典型的「空间换时间」策略。

我在项目中遇到过一个问题:一个页面用了大量 will-change: transform,结果内存直接飙到 500MB+。嗯,分层不是越多越好,得有个度。

浏览器触发分层的主要条件:

  • 有 position: fixed 或 sticky 定位的元素
  • 有 CSS 3D 变换(transform: translateZ(0))
  • 有 will-change 属性
  • 有 <video> 或 <canvas> 元素
  • 有透明度动画或滤镜效果

小技巧: 在 Chrome DevTools 的 Layers 面板里,你可以直观地看到当前页面分了多少层,以及每层的内存占用。排查卡顿时,我第一件事就是打开它。

3.2 光栅化:把「指令」变成「像素」

分层之后,每一层其实还是一堆绘制指令(比如「画一个红色矩形,从 (10,20) 到 (100,80)」)。光栅化 就是把这些指令,真正转换成屏幕上的像素点。

这个过程非常消耗计算资源。你想啊,一个 1920x1080 的屏幕,有超过 200 万个像素点。每个像素都要算颜色、透明度、覆盖关系……

浏览器为了加速,用了两个关键策略:

  1. 分块(Tiling): 把一层切成 256x256 或 512x512 的小块,并行光栅化。GPU 最擅长干这种并行活儿。
  2. 栅格化线程(Raster Threads): 浏览器会维护一个栅格化线程池,专门处理这些任务。主线程不会被阻塞。

注意: 如果某一层的绘制指令特别复杂(比如大量 SVG 路径、复杂渐变、阴影叠加),光栅化时间会急剧增加。我曾经遇到一个案例,一个页面因为用了太多 box-shadow,光栅化耗时从 2ms 飙到了 80ms——直接导致滚动掉帧。

光栅化完成后,每一层就变成了一张「位图」(Bitmap)。这些位图会被上传到 GPU 显存里,等待下一步操作。

3.3 合成:把「图层」拼成「画面」

合成(Compositing)是 GPU 阶段的最后一步。说白了,就是把所有光栅化好的位图,按照正确的顺序、位置、透明度,拼成最终你看到的画面。

这个过程在 GPU 的合成线程(Compositor Thread)里完成。它不涉及主线程,所以非常快。这也是为什么我们用 transform 和 opacity 做动画时,性能通常很好——因为它们只触发合成,不触发布局和绘制。

合成的核心操作:

  • 变换(Transform): 移动、旋转、缩放图层
  • 裁剪(Clip): 只显示图层的某一部分
  • 混合(Blend): 处理图层之间的透明度叠加
  • 遮罩(Mask): 应用 CSS clip-path 或 mask 效果

合成线程每 16.67ms(60fps)会输出一帧到屏幕。如果合成过程超过这个时间,就会出现卡顿。

3.4 完整流水线 SVG 图

下面这张图,把整个渲染流水线串起来了。你可以看到 GPU 阶段在整体流程中的位置。

渲染流水线完整流程 - GPU阶段 渲染流水线:从布局到光栅化与合成 CPU 阶段 DOM 树构建 样式计算 & 布局 GPU 阶段 分层(Layer) 光栅化(Raster) 合成阶段 合成(Composite) 显示到屏幕 关键结论 • 分层和光栅化在 GPU 阶段完成,合成在合成线程完成 • 只触发合成的属性(transform/opacity)性能最优 • 分层过多会导致内存暴涨,分层过少会导致重绘面积过大 避坑指南 我曾经接手过一个后台管理系统,表格滚动卡成 PPT。打开 Layers 面板一看, 整个页面只有 1 层,每次滚动都要重绘 2000+ 行表格。解决方案:给表格容器加 will-change: transform,强制分层,滚动立刻变丝滑。

3.5 性能关键点:哪些操作会触发 GPU 重绘?

不是所有 CSS 属性变化都会触发完整的 GPU 流水线。我整理了一张表,方便你对照:

操作类型 触发阶段 性能影响 典型属性
布局变化 CPU 布局 → GPU 光栅化 → 合成 高(全流程重走) width, height, margin, padding
绘制变化 CPU 绘制 → GPU 光栅化 → 合成 中(跳过布局) color, background, box-shadow
合成变化 仅合成 低(最快) transform, opacity

我的建议: 做动画时,优先用 transform 和 opacity。如果必须改变尺寸,考虑用 transform: scale() 代替修改 width。这个习惯能帮你避开 90% 的动画卡顿问题。

3.6 实战:如何用 DevTools 定位 GPU 阶段瓶颈?

光讲理论不行,得会动手。我一般用三个面板来排查:

  1. Performance 面板: 录制一段操作,看 Rasterizing 和 Compositing 的耗时。如果 Rasterizing 超过 10ms,说明光栅化有瓶颈。
  2. Layers 面板: 查看分层情况。如果层数超过 50,或者某一层的内存占用超过 100MB,就需要优化了。
  3. Rendering 面板: 勾选「Layer borders」,页面会显示每层的边框。绿色是合成层,黄色是普通层。如果黄色区域频繁变化,说明分层策略有问题。

一个常见的坑: 很多人以为给所有元素加 translateZ(0) 就能提升性能。其实这会让浏览器创建大量不必要的层,反而拖慢合成速度。我见过一个极端案例,一个页面加了 300 多个 translateZ(0),合成耗时从 2ms 涨到 40ms。所以,只在真正需要动画的元素上加。

好了,GPU 阶段的核心逻辑就这些。记住一句话:分层是手段,光栅化是成本,合成是出口。理解了这三者的关系,你就能在写代码时预判性能风险,而不是等卡顿了再去排查。