Chromium 最新渲染引擎--RenderingNG

简介: 前置知识简讲浏览器架构组件结构代码分析


真正的救赎,是能在苦难之中,找到生的力量心的安宁 --阿尔贝·加缪

前言

大家好,我是柒八九。好久没有更文了(2周),一来是项目活比较多,二来空余时间在系统学习其他的东西,现在还未达到写文章总结的阶段,先做一个剧透,是关于WebAssemblyVue3原理的,后期会有一些列总结和教程。 👉 敬请期待

所以总而言之,最近更文懈怠了。

但是,但是,但是,转折来了。今天给大家带来了一个关于Chromium最新渲染架构RenderNG的译文。(其实这是一些列文章中一篇,后期也会有另外文章的择重翻译)。

V8如何处理JS的文章中,我们简短的介绍过浏览器的发展历史,并且还有几个奇怪的知识点。

  1. Chromium本身就是一个浏览器
  2. Chrome浏览器一般选择Chromium的稳定版本作为它的基础

既然,ChromiumChrome之间存在不清不楚的关系。所以,针对Chromium的研究其实就是对Chrome后续最新技术方向的尝鲜。毕竟,Chrome在当前浏览器份额中一家独大。掌握了它,就相当于掌握了,浏览器最新技术的发展脉络。

last but not least,也不知道大家在平时页面开发之余,是否对浏览器渲染页面的内部机制产生过兴趣。如果有的话,想必大家肯定查阅过很多资料。

例如:

文章地址 年份 推荐⭐️数
How Browsers Work 2011 ⭐️
Inside look at modern web browser 2018 ⭐️⭐️⭐️
需要🪜
Life of a Pixel 2020 ⭐️⭐️⭐️⭐️
有点晦涩难懂
需要🪜
页面是如何生成的(宏观角度) 2022 ⭐️⭐️⭐️⭐️⭐️

其实页面是如何生成的(宏观角度)是参考各种资料的一个汇总,也算是自我总结。然后,见文知意,该篇文章是从宏观角度讲述了浏览器是如何处理页面的。

而这篇文章的原文是负责Blink中渲染引擎研发的主管所写。无论是从专业角度时间新鲜程度(2021年)都墙裂推荐

同时,为了行文方便,并且这也算是知识的二次加工,此篇文章不会原封不动的进行机械式翻译。会有一些其他资料的补充和修改。望周知。如果想看原文,文章最后会保有连接。(但是需要🪜)

时间不早了,干点正事哇。(👉 郭德纲语言包)


简明扼要

  1. 每个tab中被渲染的页面内容是一个树形结构的数据格式(frame)
  2. 每一个frame结构包含:
    - DOM 数据信息
    - CSS
    - {画布信息|Canvas}
    - 其他资源,例如 {图片|image}/{视频|video}/{字体|font}/SVG等
  3. 渲染过程主要涉及到
    1. 渲染进程中的主线程
    2. 渲染进程中的合成线程
    3. viz进程(也叫GPU进程)
  4. 使用一些CSS3的属性,可以在渲染最开始的阶段(Animate)开启硬件加速
  1. transform
  2. opacity
  3. filter
  4. will-change
  1. {布局|Layout}阶段 一些非可视化的 DOM 元素不会插入布局树中
      例如“head”元素/如果元素的 display 属性值为“none”,那么也不会显示在呈现树中(但是 visibility 属性值为“hidden”的元素仍会显示)
  2. {图层化|Layerize}/{栅格化/图片解码|Raster/Decode} 都是发生在渲染进程的合成线程
  3. 在同一时刻只有被唤起的页面才会占用浏览器进程
  4. 线程有助于实现管道并行化多重缓冲
  5. 渲染进程{主线程|Mian Thread}:
    1. 主要负责运行脚本
    2. 管理{事件循环|vent loop}
    3. 负责文档生命周期
    4. 脚本事件调度
    5. 解析HTML、CSS和其他数据格式
  6. 渲染进程{合成线程|Compositor Thread}:
    1. 处理事件的输入
    2.优化页面的内容的滚动和动画效果
    3. 对页面内容进行图层化处理
    3.对图片进行解码处理
    4. 绘画工作单元代码
    5. 进行栅格化操作
  7. 渲染进程主线程{Blink 渲染器|Blink renderer}:
  • {本地frame树|local frame tree}:
  • DOM /Canvas API:
  • {文档生命周期运行器|document lifecycle runner}
  • {输入事件探测|input event hit testing}
  1. 每个{局部框架树|local frame tree}都有自己的Blink渲染器组件

面试加油站

1.

一图胜千言


文章概要

  1. 前置知识简讲
  2. 浏览器架构
  3. 组件结构
  4. 代码分析

前置知识简讲

高屋建瓴的对渲染流程做一个归纳的话,其实它兼顾了四个方向

  1. 页面内容渲染成屏幕中的像素
  2. 处理页面中的视觉效果
  3. 处理页面{滚动|scroll}
  4. 将{输入事件|input event}有效地输送到正确的地方

每个tab中被渲染的页面内容是一个树形结构的数据格式(frame)

其中包含浏览器自带的UI(例如:输入栏、书签栏等)

{输入事件|input event}的事件流来源很多,例如触摸屏幕、键盘输入还有其他的硬件设备。

每一个frame结构包含:

- DOM 数据信息

- CSS

- {画布信息|Canvas}

- 其他资源,例如 {图片|image}/{视频|video}/{字体|font}/SVG等

一个frame结构代表一个HTML文档(包含URL信息)

被浏览器加载的 Web 应用,存在一个顶层frame,还有若干子frame。(如果有的话)。

视觉效果是一种应用于{位图|bitmap}的图形操作,例如常规的{滚动|scroll}/{剪切|clip}/{转换|transform}/{过滤|filter}/{透明处理|opacity}/{混合|blend}


浏览器架构

渲染流程

可以将渲染流程想象成一个拥有很多关键节点流水线操作模式。在每一个节点都会对来自上一个节点的“原料”进行深度加工,最终会将初始原料HTML文档渲染成屏幕中的图像信息。 (每个关键节点中都会有自己特定的数据格式,这个我们会有一篇文章介绍)

渲染过程主要涉及到

1. 渲染进程中的主线程

2. 渲染进程中的合成线程

3. viz进程(也叫GPU进程)

关键节点介绍

在渲染流程的图中,用不同颜色来标识该阶段可能会被不同的线程或者进程所执行。

颜色 所在进程/线程
绿色 渲染进程中的主线程
黄色 (黄色) 渲染进程中的合成线程
橘色 viz进程(也叫GPU进程)

在某些阶段,可能会被多个地方所执行,所以该阶段可能存在多个颜色。

然后,我们来简单介绍一下每个节点:

  1. {动画|Animate}:根据声明性的描述,随着时间的推移改变{计算样式|computed style}或者修改{属性树|property tree},然后开启GPU的硬件加速
    例如:使用一些CSS3的属性
    1. transform
    2. opacity
    3. filter
    4. will-change
.element {
    transform: rotateZ(360deg);
    transform: translate3d(0, 0, 0);
}
复制代码
  1. {样式生成|Style}: 将CSS信息嵌入到DOM树中
      👉 并且生成计算样式(computed style)
  2. {布局|Layout}: 决定屏幕中每个DOM元素的大小(size)和位置(position)
      👉 并且生成不可变fragment树 (immutable fragment tree)
      👉 非可视化的 DOM 元素不会插入布局树中
      例如“head”元素/如果元素的 display 属性值为“none”,那么也不会显示在呈现树中(但是 visibility 属性值为“hidden”的元素仍会显示)
  3. {重绘|Pre-paint}: 计算属性树(property trees)并且酌情使任何现有的显示列表(display list)和GPU纹理瓦片失效。
      👉 生成属性树(property trees)
  4. {滚动|Scroll}:通过修改属性树(上一阶段生成的数据信息),来更新文档或者可滚动元素的偏移量
  5. {绘制|Paint}: 计算显示列表(display list)用于描述如何从DOM中栅格化raster) GPU纹理瓦片
      👉 生成显示列表(display list)
  6. {提交阶段|Commit}: 将属性树显示list 复制一份,并且打包到送到合成线程

    👆🏻 上面大部分在渲染进程的主线程中

  1. {图层化|Layerize}: 将显示列表分解成一个合成的图层列表(composited layer list ),用于独立的栅格化(rasterization)和动画制作
      👉关键数据 图层列表
  2. 栅格、解码和绘图工作单元:分别将显示列表编码图像绘画工作单元代码转化为GPU纹理
      👉关键数据 GPU纹理
  3. {启动|Activate}:创建一个合成frame (compositor frame),代表如何绘制和定位GPU纹理到屏幕,以及相关的视觉效果

    👆🏻 上面大部分在渲染进程的合成线程中

  1. {合成|Aggregate}:将所有可见合成frame的合成 frame 合并为一个单一的、全局的合成器frame。
  2. {绘制|Draw}:在GPU上执行聚合的合成frame,在屏幕上创建像素。

👆🏻 上面大部分在Viz进程中


在渲染流程中,有些阶段是可以被跳过的。例如:动画、滚动等可以跳过 布局、重绘、和绘制阶段。这也解释了在渲染流程图中动画(animate)/滚动(scroll)阶段存在两个颜色(绿色/黄色)。

如果针对某些可视化效果,能够跳过布局、重绘和绘制阶段,那么它们可以直接跳过渲染主线程然后运行在合成线程中。 也就是我们常说的 硬件加速

进程和线程

CPU 进程

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

  • 渲染进程(render process):对网站进行渲染、动画、滚动和数据输入的处理。
      👉存在多个渲染进程
  • 浏览器进程(browser process):对浏览器UI进行渲染、动画和数据输入的处理,并且负责将数据转发到正确的渲染进程。
      👉 浏览器UI包含:地址栏、tab 名称和网站图标等。
      👉只有一个浏览器进程
  • Viz 进程(Viz process:聚合来自多个渲染进程和浏览器进程的数据信息。
      👉它使用GPU进行光栅和绘制
      👉只有一个Viz 进程

通常,tab和渲染进程是一一对应的,也就是说一个tab会有属于自己的渲染进程。但是,如果多个tabs之间属于同一站点并且 A 页面打开了 B页面。此时 A/B是共用一个渲染进程的。具体介绍,可以看之前写的文章。页面是如何生成的(宏观角度)

整个Chromium中只存在一个Viz 进程。毕竟,通常只有一个GPU和屏幕可供绘制。

由于浏览器可以有很多标签和窗口,而且都有浏览器UI像素需要绘制,你可能会问:为什么只有一个浏览器进程?

原因是:

在同一时刻只有被唤起的页面才会占用浏览器进程

事实上,不可见的浏览器标签大多被停用,并丢掉所有的GPU内存

线程

线程有助于实现管道并行化多重缓冲

  • 主线程(Mian Thread):1. 主要负责运行脚本2. 管理事件循环event loop)、3. 负责文档生命周期4.脚本事件调度5.解析HTML、CSS和其他数据格式
  • 主线程辅助线程(helper):创建需要编码或解码的图像位图(bitmap)和二进制数据(Blob)
  • Web Works:运行耗时脚本 Web Worker
  • 合成线程(Compositor Thread):1. 处理事件的输入2. 优化页面的内容的滚动和动画效果3. 对页面内容进行图层化处理3.对图片进行解码处理4. 绘画工作单元代码5. 进行栅格化操作。
  • 合成线程辅助线程(helper):协助Viz的光栅任务,并执行图像解码任务、绘制工作程序
  • 媒体、音频输出线程:对视频和音频流进行同步解码
      👉视频线程与主渲染管道并行执行

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

每个渲染进程只有一个主线程,即使同一网站的多个标签或frame可能最终出现在同一进程中。然而,在各种浏览器API中执行的工作是有性能隔离的。例如,Canvas API中图像位图和Blobs的生成在一个主线程辅助线程中运行。

同样地,每个渲染进程只有一个合成器线程。一般来说,只有一个并不是问题,因为合成器线程上所有真正昂贵的操作都被委托给合成器工作线程Viz进程,而且这些工作可以与输入路由、滚动或动画并行进行

合成器工作线程的数量取决于设备的能力。例如,台式机一般会使用更多的线程,因为它们有更多的CPU内核,而且比移动设备的电池限制要少。

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

- 辅助线程:将耗时任务的子任务发送给其他线程,以保持父线程对同时发生的其他请求的响应。主线程的辅助线程和合成器的辅助线程是这种技术的好例子。

- 多重缓冲:在渲染新内容的同时显示以前渲染的内容,以隐藏渲染的延迟。合成器线程使用这种技术。同样的我们在页面是如何生成的(宏观角度)中的双缓存中介绍过此类技术细节。

- 管线并行化:在多个地方同时运行渲染管线。这就是为什么滚动和动画可以很快,即使主线程的渲染更新正在发生,因为滚动和动画可以并行运行。


浏览器进程

  • 渲染和合成线程:响应浏览器用户界面中的输入,将其他输入导航到正确的渲染组件中,并且对浏览器UI进行排版和绘制
  • 渲染和合成辅助线程:执行图像解码任务或解码任务。

浏览器进程的渲染和合成线程与渲染进程的代码和功能类似,只是主线程和合成器线程被合并为一个


Viz 进程

  • GPU主线程: 将显示列表(display list)和视频帧光栅化为GPU纹理,并将合成线程生成的若干frame合并成一个并绘制到屏幕上。
  • 显示合成器线程: 聚合并优化来自每个渲染进程的合成信息,加上浏览器进程,形成一个单一的合成器frame,以便向屏幕展示。

栅格化页面绘制通常发生在同一个线程上,因为它们都依赖于GPU资源,而且很难可靠地多线程使用GPU。也就是它们都位于GPU主线程。

显示合成器是在一个不同的线程上,因为它需要在任何时候都有反应,并且不阻塞任何可能导致GPU主线程变慢的来源。导致GPU主线程速度变慢的一个原因是对非Chromium代码的调用,例如供应商特定的GPU驱动程序,这些代码可能以难以预测的方式变慢。


组件结构

在每个渲染过程主线程或合成器线程中,都有一些逻辑组件,它们以结构化的方式相互作用。

渲染进程主线程中的组件结构

  • {Blink 渲染器|Blink renderer}:
  • {本地frame树|local frame tree}:代表本地frame树和frame内的DOM
  • DOM /Canvas API:包含所有这些API的实现
  • {文档生命周期运行器|document lifecycle runner}:执行渲染操作,一直到提交阶段(commit)
  • {输入事件探测|input event hit testing}:执行命中测试以找出事件所针对的DOM元素,并运行事件调度算法和默认行为
  • {渲染事件循环调度器和运行器|rendering event loop scheduler and runner}:决定在事件循环中运行什么,什么时候运行。它以与设备显示相匹配的节奏来安排渲染的发生。

frame树是指主页面和它的子iframe,是从主页面递归生成的。

如果一个frame是在一个渲染进程中渲染的,那么它就是该进程的本地框架,否则就是远程框架

根据帧的渲染过程为其着色。在前面的图片中,绿色的圆圈是一个渲染过程中的所有帧;红色的是第二个,而蓝色的是第三个。

一个{局部框架树|local frame tree}是框架树中相同颜色的连接组件。图片中共有四个局部框架树:两个用于站点A,一个用于站点B,一个用于站点C,

每个{局部框架树|local frame tree}都有自己的Blink渲染器组件

一个局部框架树的Blink渲染器可能与其他局部框架树处于同一渲染过程中,也可能不在同一渲染过程中。


渲染进程合成线程中的组件结构

  • {数据处理器|data handler}:维护一个合成的{图层列表|layer list}、{显示列表|display lists}和{属性树|property tree}
  • {生命周期运行器|lifecycle runner}:运行渲染管道的{动画|animate}、{滚动|scroll}、{合成|composite}、{光栅化|raster}、{解码|decode}和{激活|activate}步骤
      👉动画和滚动可以在主线程和合成线程中发生
  • {输入和命中测试处理程序|input and hit test handler}:在合成线程下执行输入处理命中测试,以确定滚动手势是否可以在合成器线程上运行,以及命中测试应该针对哪个渲染过程。

代码分析

存在如下结构的文档信息。包含三个标签页(foo/bar/baz)。

Tab1:foo.com

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

foo.com引用了自身文档中的子页面 和另外一个标签页(bar.com)

Tab2:bar.com

<html>
</html>
复制代码

Tab3:baz.com

<html>
</html>
复制代码

在文章开头讲过,渲染流程兼顾了四个方向

  1. 页面内容渲染成屏幕中的像素
  2. 处理页面中的视觉效果
  3. 处理页面滚动(Scroll)
  4. 将输入事件(input event)有效地输送到正确的地方

然后我们通过几个例子来讲解一下,它们是如何实现的。

修改主frame中的DOM

该过程涉及多个iframe

1. 主frame => foo.com

2. bar.com

所以涉及到了多个合成帧的融合处理

  1. foo.com页面渲染过程中,{主线程|Main Thread}中某个{脚本|script}修改了部分DOM结构
  2. Blink 渲染器 告诉 {合成器|compositor } 它需要开始渲染操作
  3. {合成器|compositor } 告诉Viz它需要进行渲染
  4. Viz将渲染的开始信号传回给合成器。
  5. 合成器将启动信号继续向前转发给Blink渲染器
  6. 主线程{事件循环运行器|event loop runner}启动指定文档的生命周期方法
  7. 主线程将第6步运行结果发送给合成器线程 ==> 表明DOM的变更处理已经在主线程处理完
  8. 合成线程{事件循环运行器|event loop runner}启动对应合成的生命周期
  9. 如果存在{光栅任务|raster tasks}都被送到Viz进程进行光栅处理
  10. Viz在GPU中对内容进行光栅化处理
  11. Viz 将内容光栅完成后,将结果返回给合成器
  12. 一个{合成帧|compositor frame}被送往Viz显示合成器线程
  13. Vizfoo.combar.com和浏览器UI的渲染帧合并成一个合成帧
  14. Viz为绘制该合成帧做{安排|schedules}
  15. Viz将合成帧绘制到屏幕上

处理页面中视觉效果

只涉及到一个页面:即 bar.com

  1. bar.com渲染过程的合成器线程在其{事件循环运行器|event loop runner}中通过突变现有的{属性树|property trees}来触发一个动画。然后重新运行合成器的生命周期。
  2. 一个{合成帧|compositor frame}被送往Viz显示合成器线程
  3. Vizbar.com和浏览器UI的渲染帧合并成一个合成帧
  4. Viz为绘制该合成帧做{安排|schedules}
  5. Viz将合成帧绘制到屏幕上

处理页面{滚动|scroll}

只涉及到一个页面:即 baz.com

  1. {浏览器进程|browser process}产生一系列的输入事件(鼠标、触摸或键盘)
  2. 每个事件都被传送baz.com的渲染进程{合成器线程|compositor thread}
  3. 合成器决定是否向主线程转发事件信息
  4. 如果满足条件,将事件转发给渲染进程{主线程|main thread}
  5. 主线程调用特定的输入事件监听器(pointerdown、touchstar、pointermove、touchmove或wheel),看监听器是否会在事件上调用preventDefault
  6. 主线程根据事件中是否调用preventDefault来决定将事件返回给合成器
  7. 如果没有调用,将输入事件回退给浏览器进程
  8. {浏览器进程|browser process}通过将其与其他近期其他{事件|event}结合起来,将其转换为{滚动手势|scroll gesture}
  9. {滚动手势|scroll gesture}再次被传送到的渲染进程{合成器线程|compositor thread}
  10. 滚动信息在这里起作用,并且bar.com渲染进程的合成器线程在其{合成器事件循环|compositor event loop}中触发了一个动画。然后在{属性树| property tree}中改变滚动偏移,并重新运行合成器生命周期。它还告诉主线程启动一个滚动事件
  11. 一个{合成帧|compositor frame}被送往Viz显示合成器线程
  12. Vizbaz.com和浏览器UI的渲染帧合并成一个合成帧
  13. Viz为绘制该合成帧做{安排|schedules}
  14. Viz将合成帧绘制到屏幕上

处理{输入事件|input event}

bar.com中执行click事件

  1. {浏览器进程|browser process}中产生了一个输入事件(鼠标、触摸或键盘)。它执行了{命中测试|hit test},以确定bar.com 对应的渲染进程应该接收该点击事件,并将其发送到那里
  2. bar.com{合成器线程|compositor thread}将点击事件导航到bar.com的渲染{主线程|main thread},并安排一个{合成器事件循环|compositor event loop}任务来处理它
  3. bar.com的主线程的{输入事件探测|input event hit testing}通过测试来确定iframe中的哪个DOM元素被点击,并唤起一个点击事件供脚本观察。
  4. 后续的操作就和修改DOM的后续操作一样了。

后记

分享是一种态度,这篇文章,是一篇译文,算是一个自我学习过程中的一种记录和总结。主要是把自己认为重要的点,都罗列出来。同时,也是为大家节省一下排雷和踩坑的时间。当然,可能由于自己认知能力所限,有些点,没能表达很好。如果大家想看原文,“墙裂推荐”看原文。

参考资料:

  1. 原文地址 需要🪜
相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
10月前
|
移动开发 JavaScript 前端开发
3D渲染引擎介绍
3D渲染引擎介绍
284 0
|
10月前
|
Web App开发 存储 开发框架
深入理解浏览器内核 - 火狐浏览器常用插件
深入理解浏览器内核 - 火狐浏览器常用插件
106 0
|
11月前
|
Web App开发 数据采集
解决Puppeteer内置的Chromium无法自动播放音频问题
解决Puppeteer内置的Chromium无法自动播放音频问题
144 0
|
JavaScript 前端开发
electron 10行核心代码定制自己的浏览器
electron 10行核心代码定制自己的浏览器
487 0
electron 10行核心代码定制自己的浏览器
|
Web App开发 存储 移动开发
浏览器内核(渲染引擎)介绍|学习笔记
快速学习浏览器内核(渲染引擎)介绍
257 0
|
安全 JavaScript 前端开发
浏览器内核之WebKit 架构与模块
此文章是我最近在看的【WebKit 技术内幕】一书的一些理解和做的笔记。 而【WebKit 技术内幕】是基于 WebKit 的 Chromium 项目的讲解。
471 0
浏览器内核之WebKit 架构与模块
|
Web App开发 JavaScript 前端开发
Chrome和Chromium的区别
Chrome和Chromium的区别
558 0
Chrome和Chromium的区别
|
Web App开发 前端开发 JavaScript
Chrome 开发者工具 版本 65 新引入的 Overrides
Chrome 开发者工具 版本 65 新引入的 Overrides
192 0
Chrome 开发者工具 版本 65 新引入的 Overrides
|
前端开发 API 异构计算
Chromium Viz 浅析 - SkiaRenderer & SkiaOutputSurface
**关于 Viz 的系列文章** [Chromium Viz 浅析 - 介绍篇][1] [Chromium Viz 浅析 - 合成器架构篇][2] ---- Chromium 关于光栅化和合成的一些主要性能优化项目包括 OOPD (Out of Process Display Compositor,进程外 Display 合成器), OOPR (Out of Process R
2087 0
|
Web App开发 vr&ar Android开发
Chromium Viz 浅析 - 合成器架构篇
**关于 Viz 的系列文章** [Chromium Viz 浅析 - 介绍篇][1] ---- ## Mojo 快速入门 因为后面的内容需要读者对 Mojo 有一定了解,如果以前没接触过的话,这部分内容提供了一个快速的入门。 简单的说 Mojo 是一个 Client/Service 的通讯机制,支持跨进程。一个 Mojo 链接建立的过程一般如下: 1. Cli
2589 0