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 时提高稳定性。它还有利于实现安全隔离,这对 Vulkan 等 GPU API 和总体安全至关重要。

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

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

有时,系统还会提供用于解码受保护视频内容的实用程序进程。上图中未显示此流程。

线程

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

渲染流程示意图。

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

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

每个渲染进程只有一个主线程,即使来自同一网站的多个标签页或框架最终可能位于同一进程中也是如此。不过,在各种浏览器 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. 为内容从一种状态转换为另一种状态的视觉效果添加动画
  3. 响应输入进行滚动
  4. 高效地将输入路由到正确的位置,以便开发者脚本和其他子系统做出响应。

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

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

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

  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。