RenderingNG 架构

Chris Harrelson
Chris Harrelson

您将了解如何设置 RenderingNG 的组件部分,以及渲染流水线如何在这些部分中流动。

从最高级别开始,渲染任务如下:

  1. 将内容渲染为屏幕上的像素。
  2. 为内容从一种状态转换为另一种状态添加视觉效果动画
  3. 响应输入进行滚动
  4. 高效地将输入路由到正确的位置,以便开发者脚本和其他子系统做出响应。

要渲染的内容是每个浏览器标签页的帧树,以及浏览器界面。以及来自触摸屏、鼠标、键盘和其他硬件设备的一连串原始输入事件。

每个帧均包含:

  • DOM 状态
  • CSS
  • 画布
  • 外部资源,例如图片、视频、字体和 SVG

框架是指 HTML 文档及其网址。在浏览器标签页中加载的网页包含一个顶级框架、顶级文档中包含的每个 iframe 的子框架,以及它们的递归 iframe 后代。

视觉效果是指应用于位图的图形操作,例如滚动、转换、剪裁、滤镜、不透明度或混合。

架构组件

在 RenderingNG 中,这些任务在逻辑上被划分到多个阶段和代码组件中。这些组件最终会位于各种 CPU 进程、线程以及这些线程中的子组件中。每种方法都对实现所有 Web 内容的可靠性可伸缩性能可扩展性起着重要作用。

渲染管线结构

渲染流水线示意图。
箭头表示每个阶段的输入和输出。阶段用颜色标记,以表明它们执行哪个线程或进程。在某些情况下,阶段可以在多个位置执行,具体取决于具体情况,因此有些阶段会显示两种颜色。绿色阶段是渲染进程主线程;黄色是渲染进程合成器;橙色阶段是可视化进程。

渲染会在流水线中进行,并在此过程中创建多个阶段和工件。每个阶段都代表在渲染过程中执行一项明确定义的任务的代码。工件是阶段的输入或输出数据结构

各阶段如下:

  1. 动画:根据声明式时间轴,更改计算样式并随时间更改属性树
  2. 样式:将 CSS 应用于 DOM,并创建计算样式
  3. 布局:确定屏幕上 DOM 元素的大小和位置,并创建不可变 fragment 树
  4. 预绘制:根据需要计算属性树,并invalidate
  5. 滚动:通过更改属性树来更新文档和可滚动 DOM 元素的滚动偏移量。
  6. 绘制:计算一个显示列表,用于描述如何从 DOM 光栅化 GPU 纹理图块。
  7. 提交:将属性树和显示列表复制到 compositor 线程。
  8. 分层:将显示列表拆分为复合层列表,以便独立进行光栅化和动画处理。
  9. 光栅化、解码和绘制 Worklet:分别将显示列表、编码图片和绘制 Worklet 代码转换为 GPU 纹理图块
  10. 激活:创建一个合成器帧,表示如何在屏幕上绘制和放置 GPU 图块以及任何视觉效果。
  11. 汇总:将所有可见合成器帧中的合成器帧合并到单个全局合成器帧中。
  12. 绘制:在 GPU 上执行汇总的 compositor 帧,以在屏幕上创建像素。

如果不需要,可以跳过渲染流水线的阶段。例如,视觉效果和滚动动画可以跳过布局、预绘制和绘制。 因此,动画和滚动在图表中分别用黄色和绿色圆点标记。如果可以为视觉效果跳过布局、预绘制和绘制,则可以完全在 compositor 线程上运行这些操作,并跳过主线程。

此处未直接展示浏览器界面渲染,但可以将其视为此流水线的简化版本(事实上,其实现会共用大量代码)。视频(也未直接描绘)通常使用独立代码进行渲染,该代码会将帧解码为 GPU 纹理图块,然后将这些图块插入合成器帧和绘制步骤。

进程和线程结构

CPU 进程

使用多个 CPU 进程可以在网站之间和浏览器状态之间实现性能和安全性隔离,并实现与 GPU 硬件的稳定性和安全性隔离。

CPU 进程的各个部分的示意图

  • 渲染进程会为单个网站和标签页组合进行渲染、呈现动画效果、滚动和路由输入。存在多个渲染进程。
  • 浏览器进程会呈现、呈现动画效果并路由浏览器界面(包括地址栏、标签页标题和图标)的输入,并将所有剩余输入路由到适当的渲染进程。有一个浏览器进程。
  • 可视化进程会聚合来自多个渲染进程和浏览器进程的合成内容。它使用 GPU 进行光栅化和绘制。只有一个 Viz 进程。

不同的网站始终会位于不同的渲染进程中。

同一网站的多个浏览器标签页或窗口通常在不同的渲染进程中,除非这些标签页之间存在关联关系,例如其中一个标签页打开了另一个标签页。在内存压力较大的情况下,桌面版 Chromium 可能会将来自同一网站的多个标签页放入同一渲染进程,即使这些标签页之间没有关联也是如此。

在单个浏览器标签页中,来自不同网站的帧始终位于不同的渲染进程中,但来自同一网站的帧始终位于同一渲染进程中。从渲染的角度来看,使用多个渲染进程的一个重要优势是,跨网站 iframe 和标签页可以相互实现性能隔离。此外,源站还可以选择更严格的隔离

整个 Chromium 只有一个 Viz 进程,因为通常只有一个 GPU 和屏幕可供绘制。

面对 GPU 驱动程序或硬件中的 bug 时,将 Viz 拆分到自己的进程中有助于提高稳定性。它还有利于实现安全隔离,这对 Vulkan 等 GPU API 和总体安全至关重要。

由于浏览器可以有多个标签页和窗口,并且所有标签页和窗口都有可供绘制的浏览器界面像素,因此您可能会想:为什么只有一个浏览器进程呢? 原因在于,系统一次只会聚焦于其中一个标签页;事实上,不可见的浏览器标签页大多处于停用状态,并会丢弃其所有 GPU 内存。不过,越来越多的复杂浏览器界面渲染功能也在渲染进程中实现(称为 WebUI)。这并非出于性能隔离的原因,而是为了充分利用 Chromium 的网页渲染引擎的易用性。

在旧版 Android 设备上,在 WebView 中使用时,渲染进程和浏览器进程是共享的(这通常不适用于 Android 上的 Chromium,而仅适用于 WebView)。在 WebView 中,浏览器进程也与嵌入应用共享,并且 WebView 只有一个渲染进程。

有时,系统还会提供用于解码受保护视频内容的实用程序进程。前面的图表未描述此过程。

Threads

线程有助于实现性能隔离和响应能力,即使任务运行缓慢、流水线并行化和多缓冲区也是如此。

渲染流程示意图。

  • 主线程会运行脚本、渲染事件循环、文档生命周期、命中测试、脚本事件调度以及解析 HTML、CSS 和其他数据格式。
    • 主线程帮助程序可执行创建图片位图和 blob 等需要编码或解码的任务。
    • Web Worker 运行脚本,以及 OffscreenCanvas 的渲染事件循环。
  • 合成器线程可处理输入事件,对 Web 内容执行滚动和动画操作,计算 Web 内容的最佳分层,并协调图像解码、绘制 Worklet 和光栅任务。
    • 合成器线程辅助程序用于协调 Viz 光栅任务,并执行图片解码任务、绘制 Worklet 和回退光栅。
  • 媒体、多路复用器或音频输出线程:用于解码、处理和同步视频和音频流。 (请记住,视频与主渲染管道并行执行。)

将主线程和合成器线程分离对于将动画和滚动性能与主线程工作进行隔离至关重要。

每个渲染进程只有一个主线程,即使来自同一网站的多个标签页或帧最终可能位于同一进程中,也是如此。不过,在各种浏览器 API 中执行的工作具有性能隔离性。例如,Canvas API 中的图片位图和 Blob 的生成是在主线程辅助线程中运行的。

同样,每个渲染进程只有一个 compositor 线程。通常,只有一个 compositor 线程不会造成问题,因为 compositor 线程上的所有非常耗时的操作都会委托给 compositor 工作器线程或 Viz 进程,并且这项工作可以与输入路由、滚动或动画并行执行。合成器工作器线程用于协调在 Viz 进程中运行的任务,但无处不在的 GPU 加速可能会因 Chromium 无法控制的原因(例如驱动程序 bug)而失败。在这些情况下,工作器线程将在 CPU 上以回退模式执行工作。

合成器工作器线程的数量取决于设备的功能。例如,桌面设备通常会使用更多线程,因为它们具有更多 CPU 核心,并且电池电量限制较移动设备少。这是一个纵向扩容和缩容的示例。

渲染进程线程架构是三种不同优化模式的应用:

  • 辅助线程:将长时间运行的子任务发送到其他线程,以便父线程能够响应其他同时请求。主线程帮助程序和合成器帮助程序线程就是这种方法的好例子。
  • 多重缓冲:在渲染新内容时显示之前渲染的内容,以隐藏渲染延迟时间。合成器线程会使用此方法。
  • 流水线并行处理:同时在多个位置运行渲染流水线。这就是滚动和动画能够快速运行的原因;即使主线程正在进行渲染更新,滚动和动画也可以并行运行。

浏览器进程

浏览器进程示意图,显示了渲染线程和合成线程与渲染线程和合成线程辅助程序之间的关系。

  • 渲染和合成线程会响应浏览器界面中的输入,将其他输入路由到正确的渲染进程;布局和绘制浏览器界面。
  • 渲染和合成线程辅助程序会执行图片解码任务,并回退到光栅化或解码。

浏览器进程的渲染和合成线程与渲染进程的代码和功能类似,不同之处在于主线程和合成器线程合并为一个。在这种情况下,只需要一个线程,因为无需与长时间运行的主线程任务隔离性能,因为设计上没有任何长时间运行的主线程任务。

可视化流程

Viz 进程包括 GPU 主线程和显示合成器线程。

  • GPU 主线程会将显示列表和视频帧光栅化到 GPU 纹理图块,并将合成器帧绘制到屏幕上。
  • 显示屏合成器线程会汇总和优化来自每个渲染进程以及浏览器进程的合成,并将其合并到单个合成器帧中,以便呈现到屏幕上。

光栅化和绘制通常在同一线程中进行,因为它们都依赖于 GPU 资源,并且很难可靠地对 GPU 进行多线程使用(更轻松地对 GPU 进行多线程访问是开发新 Vulkan 标准的一个动机)。在 Android WebView 上,由于 WebView 的嵌入方式,因此有一个单独的操作系统级渲染线程用于绘制。其他平台未来可能会有这样的线程。

显示屏合成器位于其他线程上,因为它需要始终响应迅速,并且不会阻塞 GPU 主线程上的任何可能导致速度变慢的原因。GPU 主线程运行缓慢的一个原因是调用非 Chromium 代码(例如特定于供应商的 GPU 驱动程序),这些代码可能会以难以预测的方式运行缓慢。

组件结构

在每个渲染进程主线程或合成器线程中,都有逻辑软件组件以结构化方式相互交互。

渲染进程主线程组件

Blink 渲染程序的示意图。

在 Blink 渲染程序中:

  • 本地帧树 fragment 表示本地帧的树以及帧内的 DOM。
  • DOM 和 Canvas API 组件包含所有这些 API 的实现。
  • 文档生命周期运行程序会执行呈现流水线的步骤,包括提交步骤。
  • 输入事件命中测试和调度组件可执行命中测试以确定事件针对的是哪个 DOM 元素,并运行输入事件调度算法和默认行为。

渲染事件循环调度程序和运行程序决定在事件循环上运行什么以及何时运行。它会安排按照与设备显示屏匹配的节奏进行渲染。

框架树示意图。

本地帧树 fragment 有点复杂。回想一下,框架树是以递归方式呈现的主页面及其子 iframe。如果某个帧在某个渲染进程中渲染,该帧是该进程的本地,否则是远程帧。

您可以想象一下根据帧的渲染过程为其着色。在上图中,绿色圆圈是一次渲染进程中的所有帧;橙色圆圈是第二次渲染进程中的帧,蓝色圆圈是第三次渲染进程中的帧。

本地框架树 fragment 是框架树中具有相同颜色的连接组件。图片中包含四个本地帧树:两个用于网站 A,一个用于网站 B,一个用于网站 C。 每个本地帧树都有自己的 Blink 渲染程序组件。本地帧树的 Blink 渲染器可能与其他本地帧树位于同一渲染进程中,也可能不在同一渲染进程中。它取决于渲染进程的选择方式(如前所述)。

渲染进程合成器线程结构

显示渲染进程合成器组件的示意图。

渲染进程合成器组件包括:

  • 一种数据处理程序,用于维护复合图层列表、显示列表和属性树。
  • 一个生命周期运行程序,用于运行渲染流水线的动画、滚动、合成、光栅化、解码和激活步骤。(请注意,动画和滚动操作既可以在主线程中执行,也可以在 compositor 中执行。)
  • 输入和点击测试处理程序以合成层的分辨率执行输入处理和点击测试,以确定滚动手势是否可以在合成器线程上运行,以及应该针对哪个渲染进程点击测试。

实际架构示例

在此示例中,有三个标签页:

标签页 1:foo.com

<html>
  <iframe id=one src="foo.com/other-url"></iframe>
  <iframe  id=two src="bar.com"></iframe>
</html>

标签页 2:bar.com

<html>
 …
</html>

标签页 3:baz.com html <html> … </html>

这些标签页的进程、线程和组件结构如下所示:

标签页流程示意图。

我们来逐一了解渲染的四大任务。温馨提示:

  1. 将内容渲染到屏幕上的像素。
  2. 为内容从一种状态转换为另一种状态的视觉效果添加Animate
  3. 响应输入进行滚动
  4. 高效地将输入路由到正确的位置,以便开发者脚本和其他子系统做出响应。

如需为第一个标签页呈现更改后的 DOM,请执行以下操作:

  1. 开发者脚本会更改 foo.com 的呈现过程中的 DOM。
  2. Blink 渲染程序会告知合成器需要进行渲染。
  3. 合成器会告知 Viz 需要进行渲染。
  4. Viz 会向合成器发送渲染开始的信号。
  5. 合成器会将启动信号转发给 Blink 渲染程序。
  6. 主线程事件循环运行程序运行文档生命周期。
  7. 主线程将结果发送到合成器线程。
  8. 合成器事件循环运行程序会运行合成生命周期。
  9. 所有光栅任务都会发送到 Viz 进行光栅处理(此类任务通常不止一个)。
  10. Viz 可在 GPU 上光栅显示内容。
  11. Viz 确认光栅任务已完成。 注意:Chromium 通常不会等待光栅化完成,而是使用所谓的同步令牌,该令牌必须在光栅化任务解决之前执行第 15 步。
  12. 系统会将合成器帧发送到 Viz。
  13. Viz 会汇总 foo.com 渲染进程、bar.com iframe 渲染进程和浏览器界面的合成器帧。
  14. Viz 会安排抽签。
  15. Viz 会将汇总的合成器帧绘制到屏幕上。

如需在第二个标签页中为 CSS 转换过渡添加动画,请执行以下操作:

  1. bar.com 渲染进程的合成器线程通过更改现有的属性树,在其合成器事件循环中触发动画。然后,系统会重新运行合成器生命周期。(可能会发生光栅和解码任务,但此处并未说明)。
  2. 合成器帧会发送到 Viz。
  3. Viz 会汇总 foo.com 渲染进程、bar.com 渲染进程和浏览器界面的 compositor 帧。
  4. Viz 会安排抽签。
  5. Viz 将聚合的合成器帧绘制到屏幕上。

如需在第三个标签页上滚动网页,请执行以下操作:

  1. 一系列 input 事件(鼠标、触摸或键盘事件)传入浏览器进程。
  2. 每个事件都会路由到 baz.com 的渲染进程合成器线程。
  3. 合成器确定主线程是否需要了解该事件。
  4. 系统会根据需要将事件发送到主线程。
  5. 主线程会触发 input 事件监听器(pointerdowntouchstarpointermovetouchmovewheel),以确定监听器是否会对事件调用 preventDefault
  6. 主线程会返回是否向 compositor 调用了 preventDefault
  7. 否则,系统会将输入事件发送回浏览器进程。
  8. 浏览器进程会将其与其他近期事件组合,将其转换为滚动手势。
  9. 滚动手势再次发送到 baz.com 的渲染进程合成器线程,
  10. 滚动操作会应用到该位置,并且 bar.com 渲染进程的 compositor 线程会在其 compositor 事件循环中触发动画。然后,这会更改属性树中的滚动偏移量,并重新运行 compositor 生命周期。它还会告知主线程触发 scroll 事件(此处未介绍)。
  11. 系统会将合成器帧发送到 Viz。
  12. Viz 会汇总 foo.com 渲染进程、bar.com 渲染进程和浏览器界面的 compositor 帧。
  13. Viz 安排了平局。
  14. Viz 会将汇总的合成器帧绘制到屏幕上。

如需在第一个标签页的 iframe #two 中的超链接上路由 click 事件,请执行以下操作:

  1. 浏览器进程会发生 input 事件(鼠标、触摸或键盘)。它会执行近似的点击测试,以确定 bar.com iframe 呈现进程应接收点击,并将其发送到该进程。
  2. bar.com 的 compositor 线程会将 click 事件路由到 bar.com 的主线程,并安排渲染事件循环任务来处理该事件。
  3. bar.com 主线程的输入事件处理程序会执行点击测试,以确定点击了 iframe 中的哪个 DOM 元素,并触发 click 事件以供脚本观察。没有听到 preventDefault,它会转到超链接。
  4. 在超链接的目标网页加载后,系统会呈现新状态,步骤与上例中的“呈现更改后的 DOM”类似。(此处未显示这些后续更改。)

要点总结

系统可能需要大量时间才能记住和内化渲染的运作方式。

最重要的收获是,渲染流水线经过精心模块化处理和细节处理,已拆分为多个自包含的组件。然后,这些组件已拆分到并行进程和线程之间,以最大限度地提高可扩缩性能可扩展性的机会。

每个组件都对实现现代 Web 应用的性能和功能发挥着至关重要的作用。

继续阅读,了解关键数据结构。这些数据结构对 RenderingNG 来说,与代码组件一样重要。


插图作者:Una Kravets。