深入浏览器引擎 III:Chromium分层合成 vs Firefox WebRender的GPU革命

深入浏览器引擎 III:Chromium分层合成 vs Firefox WebRender的GPU革命

渲染管线

将 HTML 从源代码转化成屏幕像素这一过程称为渲染管线,不同渲染引擎的实现方式有所区别,但是大体上都可以分为五个步骤:

  1. 解析
  2. 样式计算
  3. 布局
  4. 绘制
  5. 合成 & 渲染

![[./images/Pasted image 20250714205605.png]]

解析

HTML 解析器将 HTML 文档源代码转换为 文档对象模型(DOM)。DOM 具有两个关键作用:

  1. 提供 JavaScript 编程接口: DOM 作为 JavaScript 操作页面内容、结构和样式的主要接口,通过修改 DOM 可以动态改变页面的最终呈现效果。
  2. 作为渲染管线的核心数据结构: DOM 是浏览器将 HTML 文档转换为屏幕像素的整个渲染管线中的基础数据结构和关键输入。
    HTML 解析器的工作原理在前面的[[深入浏览器引擎 II:HTML 解析全流程——从字节流到 DOM 构建]]一章中已经详细介绍过,这里不再赘述。

![[Pasted image 20250712201804.png]]

样式计算

在样式计算中,CSS 引擎会将 CSS 规则应用到对应的 DOM 节点上,从而生成渲染树(Render Tree)。渲染树在 DOM 树的基础上还包含了每个元素节点的 CSS 样式。

CSS 全称层叠样式表,它具有层叠性、继承性和优先级这三大特征。

  • 层叠性:样式冲突时,优先级高的样式生效。
  • 继承性:某些样式(如 color、font-size)会自动继承父元素节点。
  • 多个 css 规则作用一个元素时,通过权重来决定哪个规则生效。

这些特性共同作用,决定了最终渲染在页面上的样式结果。在样式计算阶段,CSS 引擎计算所有的 CSS 规则,并将匹配的 CSS 规则的样式属性添加到 DOM 树对应的节点中。

![[Pasted image 20250712202236.png]]

具体来说,CSS 引擎会逐个遍历每个 DOM 节点,并找出该 DOM 节点的样式。作为此过程的一部分,它会为 DOM 节点的每个 CSS 属性赋予一个默认值,即使样式表没有为该属性声明值。

对于每个 DOM 节点,CSS 引擎需要遍历所有规则来执行选择器匹配。但是对于大多数节点,这种匹配可能不会经常改变。例如,如果用户将鼠标悬停在父节点上,与其匹配的规则可能会发生变化。由于 CSS 的继承特性,我们仍然需要重新计算其后代的样式来处理属性继承,但与这些后代元素自身所匹配的规则可能不会改变。

因此 CSS 引擎会为 DOM 节点找到所有匹配的规则,并按照优先级排序,形成一个链表结构。多个链表由组成了一颗树状的数据结构,我们称为 CSS 规则树。CSS 规则树只保存了元素自身匹配的结构,不会保存继承的属性。

在这棵 CSS 规则树中,叶子节点就是优先级最高的规则,根据层级的从高到低,规则的优先级也由低到高,DOM 节点将保存叶子节点的指针,在本例中是 div#warning

![[Pasted image 20250712212433.png]]

当父节点改变时,引擎会快速检查对父级的更改是否会改变与子级匹配的规则。如果没有,那么对于所有后代节点,直接通过保存的叶子节点的指针自下而上得遍历到根节点,就能获得匹配规则的完整列表。

CSS 规则树缓存了每个 DOM 节点匹配的 css 规则列表,当重新样式计算时,可以快速地找到所有匹配规则的完整列表,避免再次遍历所有规则来确认匹配的规则。不过,这只是作为重新样式计算的优化手段,在初次样式计算时,仍然需要遍历所有规则。

布局

接下来,渲染引擎会计算渲染树,来确定每一个元素的在页面中的位置和宽高等几何属性,这会生成布局树(Layout Tree),布局树上仅包含可见的元素节点,不可见的元素节点(如 display:none)不会保存在布局树中。

![[Pasted image 20250712204048.png]]

绘制 & 合成 (Chromium)

不同浏览器内核对于绘制和合成这两步的实现思路和方式差异很大,在这一节,我们以 Chromium 为例。

绘制

在这一过程中,渲染引擎会将页面划分成多个图层,这类似 PS 的图层概念。一个图层可以看成是一张二维的纸张(x 轴 * y 轴),而多个图层会上下堆叠在一起(z 轴),从而形成了3维空间。

图层没有父子关系,渲染引擎会将图层由后到前地保存在一个平级列表中,Chromium 官方将其称为层树(Layer Tree),因为这曾经是一颗树。

渲染引擎会遍历层树,为每个图层生成绘制指令列表,并将多个图层的绘制指令列表的集合交给合成线程处理。

注意:绘制实际上是生成绘制指令,最终需要经过合成线程和 GPU 进程的共同操作来转化为像素并渲染到屏幕上。

![[Pasted image 20250712210757.png]]

合成

在 Chromium 中,上面四个步骤通常是在主线程中完成的,而最后一步合成操作,渲染引擎通常会交由合成线程来完成,以尽量减少主线程的工作。合成线程会通过与 GPU 进程的共同协作来实现将绘制指令转换成像素渲染到屏幕上。

合成线程会将图层进一步划分更小的图块,然后交由 GPU 进程中的专门进行光栅化(rasterization)的工作线程池进行光栅化,从而输出为位图。在 Chromium 中,这是由 Skia 图像库实现的,而 Firefox 中,这由 WebRender 直接调用 GPU 实现。

在这一过程中,离视口越近的图块光栅化的优先级越高,视口内的图块光栅化优先级最高,以便在滚动时能够生成流畅的滚动动画效果。

光栅化是指将图形或绘制指令转换为像素级的图像数据(位图),也就是把“该画什么”变成“在屏幕的哪些像素上画什么颜色”。在浏览器中,它是将图层内容转为可以上传给 GPU 显示的图像块的过程。

GPU 进程完成光栅化后,将生成好的位图交还给合成线程,然后合成线程将其合成为帧
,然后交给 GPU 进程中的 Viz 线程,Viz 线程会同步这些帧并处理依赖关系,按照顺序通过 Skia 图像库将这些帧渲染到屏幕上,形成动画帧。

![[Pasted image 20250712211133.png]]

绘制 & 合成 (FireFox)

Firefox 在绘制和合成这两步操作和 Chromium 有着显著的差异。

图层的弊端

在前面,我们讲到绘制过程中会划分图层,生成层树,按照图层来生成绘制指令。划分图层的优势总结起来有两点:

  1. 一个图层发生变化后,只需要重新布局、绘制和合成该图层,不会影响到其他图层。
  2. 划分图层后,可以将多个图层并行地进行光栅化,提高效率。

但在 Firefox 开发团队看来,使用层级也有弊端。它们会占用大量内存,实际上还会降低运行速度。浏览器需要在合理的地方合并层级,但很难判断在哪里合理。

常用的 will-changetransform 等属性能够提升图层,但是也可能会导致滥用。如果图层过多,这些图层会填满内存,并且传输到合成器需要很长时间。

![[Pasted image 20250714222955.png]]

如果图层过大且没有被合理的拆分,那么这个图层会被不断重绘并传输到合成器,合成器会在不做任何改变的情况下进行合成。这意味着你需要绘制的量加倍了,每个像素都要修改两次,却没有任何效果。如果直接渲染页面,省去合成步骤,速度会更快。

![[Pasted image 20250714223149.png]]

很多情况下,图层的作用并不大。例如,如果你为背景颜色添加动画效果,整个图层无论如何都必须重新绘制。这些图层只能对少量的 CSS 属性有所帮助。

即使大多数帧都处于最佳情况(即它们只占用帧预算的一小部分),仍然可能会出现运动不流畅的情况。对于可察觉的卡顿,只需几帧处于最差情况即可。这些场景被称为性能悬崖。你的应用看似运行良好,直到遇到这些最坏的情况(例如背景颜色动画),你的应用帧率突然跌落到临界点。

![[Pasted image 20250714223316.png]]

渲染

基于以上问题,firefox 自研了 Webrender 引擎,相对于 Chromium 的绘制和合成步骤有一些显著的改变:

  1. 不再有图层,而是采用统一的 3D 坐标系的概念,布局现在提供的是显示列表(display list) 而不是布局树。
  2. 绘制和合成之间不再有区别,它们被合并在渲染这一个步骤中。
  3. GPU 将承担更多任务

![[Pasted image 20250716220619.png]]

现在,没有图层,只有一个统一的 3D 坐标系,一切元素都通过 x、y、z 坐标来确定位置,这些元素都位置信息保存在显示列表中,由点成线,由线成面。

![[Pasted image 20250716220447.png]]

显示列表是一组高级绘图指令。它告诉我们需要绘制什么,而无需特定于任何图形 API。但是 GPU 无法直接使用这部分代码,而是通过基于 WebRender 在 CPU 上创建的 RenderBackend 线程来将指令转换成 GPU 调用,再通过合成线程按批次中转传递给 GPU 后,才能被调用。

![[Pasted image 20250716221914.png]]

通过 WebRender 引擎,CPU将不再需要频繁地进行绘制、图层计算和合成等操作:

  1. 主线程不再需要完成全部的绘制任务,大部分的绘制将由 GPU 承担。(部分绘制任务仍然只能考 CPU 完成)
  2. 合成线程不再需要频繁地划分图块和合成,现在只需要同步帧率和批量转发 RenderBackend 线程传递的 CPU 调用即可。
  3. GPU 现在承担大部分的绘制、光栅化等工作,CPU 得到释放。

随着硬件的发展,GPU 的能力越来越强大,通过将更多的任务交给GPU,可以让 CPU 更专注处理 JavaScript 执行和垃圾回收等工作,这对性能有非常积极的影响。

区别总结

ChromiumFirefox
浏览器内核ChromiumGecko
渲染引擎BlinkGecko
图形渲染引擎SkiaWebRender
布局输出布局树输出显示列表
绘制主要由CPU 完成由GPU + CPU 共同完成
合成负责划分图块、合成等只负责同步帧率和转发
CPU线程主线程+合成线程主线程+RenderBackend 线程+合成线程
优化策略划分图层,图层间的回流重绘互不影响采用3D 坐标系,将更多任务交给 GPU
性能特点在复杂 DOM 场景下可能更快(分块渲染)。但是大量动画场景下可能出现性能悬崖。在 GPU 加速场景(如大量动画)中更稳定。

参考

Life of a Pixel
https://developer.chrome.com/blog/inside-browser-part1?hl=zh-cn
[The whole web at maximum FPS: How WebRender gets rid of jank)

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注