10、绘制(Painting)与合成(Compositing):绘制指令生成、PaintLayer 与 GraphicsLayer、合成器的工作原理、硬件加速与光栅化

好,我们进入第十章。这一章聊的是浏览器内核里最“出活儿”的阶段——绘制与合成。

你想想看,前面我们解析了HTML,构建了DOM树,算好了样式,排好了布局。但屏幕上还是空的。为什么?因为还没画出来。

绘制(Painting)和合成(Compositing),就是把前面所有计算结果变成屏幕上像素的最后两步。我个人觉得,这是整个渲染管线里最容易被低估的环节。很多人以为布局算完了就差不多了,其实真正的性能瓶颈往往就藏在这里。

10.1 绘制指令生成:从布局树到绘制列表

布局完成之后,WebKit 会遍历布局树(Layout Tree),为每个需要绘制的节点生成绘制指令。这些指令不会直接操作像素,而是被记录成一个绘制列表(Display List)。

为什么要搞个列表?直接画不行吗?

嗯,这里要注意。直接画确实可以,但效率极低。因为页面经常需要局部重绘,如果每次都从头画一遍,CPU 和 GPU 都受不了。绘制列表的好处是:它把绘制过程“指令化”了,可以按需回放,也可以做增量更新。

绘制指令大概长这样:

// 伪代码:绘制列表中的指令示例
PaintOp{
  type: DrawRect,
  bounds: (10, 20, 200, 100),
  color: rgba(255, 0, 0, 0.8)
}
PaintOp{
  type: DrawText,
  position: (20, 50),
  text: "Hello WebKit",
  font: "16px sans-serif"
}
PaintOp{
  type: DrawImage,
  src: "logo.png",
  destRect: (100, 200, 50, 50)
}

我在项目中遇到过一个问题:某个页面滚动时卡顿严重,一查发现绘制列表里有大量 DrawImage 指令,每帧都在重新解码图片。后来改成用缓存图层,问题就解决了。所以绘制指令的生成质量,直接影响后续性能。

核心要点:绘制指令是“延迟执行”的。先记录下来,等到真正需要显示时才去光栅化。这个设计让浏览器可以批量处理绘制操作,避免重复劳动。

10.2 PaintLayer 与 GraphicsLayer:分层是合成的基石

绘制指令生成之后,WebKit 会把这些指令分配到不同的 PaintLayer(绘制层)中。每个 PaintLayer 对应一个渲染上下文,比如一个带有 position: absoluteopacity 属性的元素,就会自成一个绘制层。

但 PaintLayer 还不是最终交给 GPU 的东西。在它之上,还有 GraphicsLayer(图形层)。

这两者的区别是什么?

说白了:PaintLayer 是逻辑上的分层,GraphicsLayer 是物理上的分层。一个 GraphicsLayer 可以包含多个 PaintLayer,但最终每个 GraphicsLayer 都会对应一个 GPU 纹理。

我习惯用 Chrome 的开发者工具看图层的分配情况。打开“Layers”面板,你能看到页面被分成了多少块。有一次我优化一个动画页面,发现某个元素因为加了 will-change: transform,被单独提升为一个 GraphicsLayer。动画流畅了,但内存占用也上去了。这就是典型的“用空间换时间”。

特性 PaintLayer GraphicsLayer
层级 逻辑层,基于 DOM 结构 物理层,对应 GPU 纹理
触发条件 position、opacity、overflow 等 3D 变换、video、canvas、will-change 等
绘制方式 CPU 绘制指令 GPU 纹理合成
内存开销 高(每个图层一张纹理)
避坑指南:我曾经为了追求动画流畅,给大量元素加了 will-change: transform,结果内存暴涨,低端设备直接崩溃。后来我学乖了:只在确实需要动画的元素上用,而且动画结束后及时移除。

10.3 合成器的工作原理:把图层拼成画面

合成器(Compositor)是负责把多个 GraphicsLayer 合并成最终画面的组件。它运行在独立的合成线程中,不阻塞主线程。

为什么需要合成器?

你想想看,如果页面有 10 个图层,每个图层都有自己的纹理。合成器的工作就是告诉 GPU:“把图层 1 放在最下面,图层 2 放在它上面偏左 10 像素,图层 3 半透明叠在上面……”

合成器的工作原理可以概括为三步:

  1. 收集图层信息:从主线程拿到所有 GraphicsLayer 的变换矩阵、裁剪区域、透明度等属性。
  2. 计算合成顺序:根据 z-index、3D 变换等确定图层的叠放顺序。
  3. 提交给 GPU:生成合成指令,交给 GPU 执行。

这里有个关键点:合成器只处理图层的变换和透明度,不重新绘制内容。所以如果你只是滚动页面或者做位移动画,合成器可以独立完成,完全不需要主线程参与。这就是为什么 transform 动画比 left/top 动画流畅得多。

注意:合成器虽然快,但它不是万能的。如果图层内容本身需要重绘(比如文字变化、颜色变化),那还是得走主线程的绘制流程。合成器只负责“拼图”,不负责“画画”。

10.4 硬件加速与光栅化:从指令到像素

光栅化(Rasterization)是把绘制指令变成实际像素的过程。在 WebKit 中,光栅化有两种方式:

  • 软件光栅化:用 CPU 逐像素计算,生成位图。
  • 硬件加速光栅化:用 GPU 并行计算,生成纹理。

现代浏览器几乎都默认走硬件加速。为什么呢?因为 GPU 天生适合做并行计算。一个 1920x1080 的页面,有 200 多万个像素,CPU 要一个一个算,GPU 可以同时算几百个。

硬件加速的流程大致是这样的:

  1. 主线程生成绘制指令,传给合成线程。
  2. 合成线程把指令分块(Tile),每块通常是 256x256 或 512x512 像素。
  3. 每个 Tile 被提交给 GPU 进行光栅化。
  4. 光栅化完成后,Tile 变成纹理,存在 GPU 显存中。
  5. 合成器把这些纹理拼成最终画面。

分块的好处很明显:如果页面只变化了一小块区域,只需要重新光栅化对应的 Tile,其他 Tile 保持不变。我在优化一个长列表页面时,就利用了这个特性——只更新可见区域的 Tile,滚动性能提升了 40%。

关键认知:硬件加速不是“免费午餐”。每个 GraphicsLayer 都需要占用 GPU 显存。图层越多,显存消耗越大。在移动设备上,显存是稀缺资源,过度分层会导致 OOM(内存溢出)。

10.5 知识体系总览

下面这张图总结了绘制与合成的完整流程:

绘制与合成完整流程 布局树 Layout Tree 绘制指令生成 Display List PaintLayer 绘制层 GraphicsLayer 图形层(GPU纹理) 分块(Tiling) 256x256 像素块 光栅化 CPU/GPU 转像素 合成器(Compositor) 独立线程运行 GPU 合成 纹理拼接 → 帧缓冲区 屏幕像素输出 硬件加速路径:GPU 参与光栅化与合成

从这张图你可以看到,整个流程是分阶段、分线程的。主线程负责生成绘制指令和 PaintLayer,合成线程负责 GraphicsLayer 的管理和合成。这种分工让浏览器能充分利用多核 CPU 和 GPU 的能力。

个人经验:我调试性能问题时,第一步就是看图层的数量。如果图层超过 50 个,基本可以断定有性能隐患。第二步看光栅化耗时,如果单帧光栅化超过 16ms,那就得考虑减少绘制区域或者优化绘制指令。

好了,这一章的内容就到这里。绘制与合成是浏览器渲染管线中最接近硬件的部分,理解它们的工作原理,能帮你写出更流畅的 Web 应用。下一章我们会深入探讨内存管理与缓存策略,敬请期待。


公众号:蓝海资料掘金营,微信deep3321