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 万个像素点。每个像素都要算颜色、透明度、覆盖关系……
浏览器为了加速,用了两个关键策略:
- 分块(Tiling): 把一层切成 256x256 或 512x512 的小块,并行光栅化。GPU 最擅长干这种并行活儿。
- 栅格化线程(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 阶段在整体流程中的位置。
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 阶段瓶颈?
光讲理论不行,得会动手。我一般用三个面板来排查:
- Performance 面板: 录制一段操作,看 Rasterizing 和 Compositing 的耗时。如果 Rasterizing 超过 10ms,说明光栅化有瓶颈。
- Layers 面板: 查看分层情况。如果层数超过 50,或者某一层的内存占用超过 100MB,就需要优化了。
- Rendering 面板: 勾选「Layer borders」,页面会显示每层的边框。绿色是合成层,黄色是普通层。如果黄色区域频繁变化,说明分层策略有问题。
一个常见的坑: 很多人以为给所有元素加 translateZ(0) 就能提升性能。其实这会让浏览器创建大量不必要的层,反而拖慢合成速度。我见过一个极端案例,一个页面加了 300 多个 translateZ(0),合成耗时从 2ms 涨到 40ms。所以,只在真正需要动画的元素上加。
好了,GPU 阶段的核心逻辑就这些。记住一句话:分层是手段,光栅化是成本,合成是出口。理解了这三者的关系,你就能在写代码时预判性能风险,而不是等卡顿了再去排查。