2. 渲染流水线剖析:CPU、GPU、合成器,谁在拖后腿?
做性能优化,最怕的就是瞎猜。
卡顿了,到底是 JS 太慢?还是绘制太复杂?还是图层合成出了问题?
我见过太多团队,一卡就怼 JS,结果优化了半天,帧率纹丝不动。为什么?因为他们没搞懂渲染流水线里,到底是谁在拖后腿。
今天我们就来把这根流水线拆开,看看 CPU、GPU、合成器各自扮演什么角色。你只有知道瓶颈在哪,才能对症下药。
2.1 一帧画面的诞生:从 JS 到像素
浏览器渲染一帧,不是一步到位的。它走的是个流水线,大致分五个阶段:
- JavaScript:执行你的脚本,处理事件,触发动画。
- Style:计算匹配的 CSS 规则,算出每个元素的最终样式。
- Layout:计算每个元素的位置和大小,也就是几何信息。
- Paint:把元素绘制成位图,比如颜色、阴影、文字。
- Composite:把各个图层按顺序合成到屏幕上。
这五个阶段,前四个主要在 CPU 上跑,Paint 阶段会生成位图交给 GPU,最后的 Composite 则是 合成器线程 的活。
核心认知: 不是每一帧都会走完所有阶段。你改了什么属性,决定了流水线走到哪一步就停。
举个例子,你改 transform 或 opacity,浏览器可以跳过 Layout 和 Paint,直接走 Composite。这就是为什么动画用 transform 比用 left 快得多。我有个项目,把列表动画从 left 改成 transform,帧率直接从 20fps 飙到 55fps。嗯,差别就这么大。
2.2 CPU:重活累活它全包
CPU 是渲染流水线的起点,也是最容易出瓶颈的地方。
它负责:
- 解析 HTML/CSS/JS:构建 DOM 树和 CSSOM 树。
- 执行 JavaScript:你的业务逻辑、DOM 操作、动画循环都在这里。
- 计算样式和布局:这是纯计算密集型任务。
- 生成绘制指令:告诉 GPU 怎么画。
CPU 一旦忙不过来,帧率就崩了。最常见的场景就是 长任务——一个 JS 任务执行超过 50ms,就会卡掉一帧甚至多帧。
我的习惯: 打开 Performance 面板,看 Main 线程的火焰图。如果看到一块块又宽又高的黄色长条,那就是 JS 在拖后腿。我曾经在一个数据可视化项目里,发现一个排序函数占了 80ms,拆成 requestAnimationFrame 分批执行后,卡顿立刻消失。
CPU 瓶颈的典型特征:
- 火焰图上 Main 线程密集,Task 时长超过 16ms。
- Layout 或 Style 阶段耗时异常高。
- 滚动或动画时,CPU 占用率飙升。
2.3 GPU:画图快,但别让它太累
GPU 擅长并行处理,专门负责把位图合成到屏幕上。它不擅长做复杂的逻辑计算,但画像素是它的强项。
GPU 的瓶颈通常来自:
- 过多的绘制指令:比如复杂的 CSS 滤镜、大量阴影、Canvas 绘制。
- 纹理上传:把位图从 CPU 内存传到 GPU 显存,这个过程很慢。
- 图层过多:每个图层都需要 GPU 单独处理。
怎么判断是 GPU 瓶颈?
看 Performance 面板的 GPU 时间线。如果 GPU 占用率长期在 80% 以上,或者 Frames 面板显示 GPU 帧耗时很高,那问题就在 GPU。
我曾经踩过的坑: 一个 H5 活动页,用了大量 CSS 滤镜和渐变,在低端机上卡成 PPT。一查,GPU 帧耗时 40ms。去掉几个不必要的 filter: blur() 后,帧耗时降到 8ms。记住,GPU 不是万能的,复杂的视觉效果要克制。
2.4 合成器:最后的把关人
合成器线程是 Chrome 里一个独立的线程,专门负责把各个图层拼到一起。它不跑 JS,不计算布局,只做合成。
合成器的优势:
- 它独立于主线程,即使主线程卡了,合成器也能继续工作。
- 滚动和
transform动画可以完全在合成器线程完成,不碰主线程。
但合成器也有瓶颈:
- 图层爆炸:每个
will-change: transform或translateZ(0)都会创建一个新图层。图层太多,合成器处理不过来。 - 大图层:一个 4K 分辨率的图层,合成器处理起来很吃力。
- 频繁的图层重建:比如你不断改变元素的
z-index,导致图层层级变化,合成器就得重新计算。
关键指标: 在 Chrome DevTools 的 Rendering 面板里,勾选 "Layer borders"。如果看到页面上有大量橙色或蓝色的边框,说明图层太多了。正常页面,图层数控制在 10-20 个以内比较健康。
2.5 一张图看懂渲染流水线
下面这张图,是我自己总结的渲染流水线全貌。你保存下来,以后做性能分析时对照着看。
2.6 如何快速定位瓶颈?
知道了谁在干活,接下来就是怎么抓出那个拖后腿的家伙。我一般按这个步骤来:
| 现象 | 可能瓶颈 | 验证方法 |
|---|---|---|
| JS 执行时间长,主线程密集 | CPU(JS 执行) | Performance 面板看 Main 线程火焰图 |
| Layout 或 Style 耗时高 | CPU(布局/样式计算) | 看 Layout 和 Style 事件时长 |
| GPU 帧耗时高,GPU 占用率满 | GPU | Performance 面板看 GPU 时间线 |
| 滚动卡顿,但主线程空闲 | 合成器 | Rendering 面板看 Layer borders,检查图层数 |
| 动画掉帧,但 CPU/GPU 都不高 | 合成器(图层重建) | 看 Layers 面板,检查图层是否频繁变化 |
一个小技巧: 在 Chrome 里按 Ctrl+Shift+P,输入 "Rendering",打开 "Frame Rendering Stats"。这个面板会实时显示每一帧的耗时,以及是主线程还是合成器线程在干活。我调试时基本一直开着它。
2.7 避坑指南:别乱用 will-change
很多人知道 will-change: transform 可以提升动画性能,于是到处加。结果呢?图层爆炸,合成器累死,反而更卡。
我曾经接手过一个项目,页面里每个卡片都加了 will-change: transform,图层数飙到 80 多个。低端手机上一滚动就掉帧。去掉不必要的 will-change 后,图层降到 12 个,帧率稳定 60fps。
记住: will-change 是告诉浏览器「这个元素要变了,提前准备」。但它会创建新图层,占用 GPU 内存。只在真正需要动画的元素上用,而且动画结束后记得移除。
好了,渲染流水线就拆到这里。你想想看,下次遇到卡顿,是不是可以先问问自己:是 CPU 在算?是 GPU 在画?还是合成器在拼?找到答案,优化就成功了一半。