浏览器渲染全流程

1,654 阅读30分钟

什么是浏览器?

浏览器是用来检索、展示以及传递Web信息资源的应用程序

浏览器有哪些组件组成?

  1. 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
  2. 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
  3. 渲染引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTMLCSS 内容,并将解析后的内容显示在屏幕上。
  4. 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  5. 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  6. JavaScript 解释器 - 用于解析和执行 JavaScript 代码。
  7. 数据存储 - 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。

image.png

浏览器发展历史

image.png

浏览器内核及分类

浏览器内核是浏览器的核心,也称“渲染引擎”,用来解释网页语法并渲染到网页上。浏览器内核决定了浏览器该如何显示网页内容以及页面的格式信息。

常见内核

Trident内核 - 代表产品为Internet Explorer,又称其为IE内核。Trident(又称为MSHTML),是微软开发的一种排版引擎。使用Trident渲染引擎的浏览器有:IE、傲游、世界之窗浏览器、腾讯、Avant

Gecko内核 - 代表作品为Mozilla Firefox。Gecko是一套开放源代码的、以C++编写的网页排版引擎,是最流行的排版引擎之一,仅次于Trident。使用它的最著名浏览器有Firefox

WebKit内核 - Webkit(Safari内核,Chrome内核原型,开源) : 它是苹果公司自己的内核,也是苹果的Safari浏览器使用的内核。(Webkit引擎包含WebCore排版引擎及JavaScriptCore解析引擎)

Blink内核 - Blink是一个由Google和Opera Software开发的浏览器排版引擎,这一渲染引擎是开源引擎WebKit中WebCore组件的一个分支,并且在Chrome(28及往后版本)、Opera(15及往后版本)浏览器中使用

Presto内核 - Presto(Opera前内核) (已废弃): Opera12.17及更早版本曾经采用的内核,现已停止开发并废弃,改用Google Chrome的Blink内核。

在正式进入全流程之前,我们需要提前知道一些概念

HTTP 通信协议

HTTP通信协议是超文本传输协议的简称,它是属于浏览器和Web服务器之间的通信协议,建立在TCP/IP基础之上,用于传输浏览器到服务器之间的HTTP请求和响应。它不仅需要保证传输网络文档的正确性,同时还确定文档显示的先后顺序。

进程

具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

线程

进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

image.png

一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。

为什么要讲进程和线程呢?

目前市场上大部分的浏览器都是多进程(早期浏览器为单进程)

Chrome 浏览器包括浏览器主进程、缓存进程、网络进程、GPU进程、插件进程、渲染器进程

截屏2022-02-23 下午4.26.12.png

渲染流程:

大致的流程:

渲染引擎从网络层获取请求文档的内容(内容的大小一般限制在 8000 个块以内)。

渲染引擎开始解析 HTML 文档,先转换为 token,后将 token 逐个转化成“内容树”上的 DOM 节点。

同时也会解析外部 CSS 文件以及样式元素中的样式数据。

HTML 中这些带有视觉指令的样式信息将用于创建另一个树结构: layout 树。layout 树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。

layout 树构建完毕之后,进入布局处理阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标。

下一个阶段是绘制,呈现引擎会遍历 layout 树,由用户界面后端层将每个节点绘制出来。

image.png


浏览器进程中的UI线程根据浏览器输入的内容进行不同进程的执行,如果输入的是网址,则UI线程会启动一个网络线程来请求DNS域名解析获取数据,如果输入的是关键词搜索直接跳转至默认搜索引擎进行关键词搜索。接下来,我们进入DNS获取数据后的渲染流程。

当网络线程获取到数据后,会根据SafeBrowsing来检查站点是否为恶意站点,如果是恶意站点,则会弹出安全提示,浏览器会阻止用户继续访问,当然用户也可以强行继续访问。(SafeBrowsing可自行查阅谷歌词典,为谷歌开发一套站点安全系统。)

image.png

检测通过后,网络线程会通知UI线程数据请求完毕,可以进入下一步了,然后UI线程会创建一个渲染器进程(Render Thread)来渲染页面。浏览器进程将会通过IPC管道将数据传递给渲染进程,正式进行页面的渲染,将js、html、css、image等资源渲染成用户可以交互的web界面。

DOM

解析为DOM

HTML嵌套解析,解析时候解析为数据对象映射反应这里的嵌套模型

DOM(Document Object Model)是一棵树,树有父子,邻居的关系,而且这棵树是暴露API给JS调用,JS可以查询和修改这棵树。JS引擎V8通过bindings的系统将DOM包装为DOM API供给Web开发者调用

文档可能包含多个DOM树

如上图的示例,自定义元素custom element有shadow tree。ShadowRoot的子元素其实被嵌入到slot元素里了,这跟各大前端框架的slot其实很像。

其实最后是在遍历树后合成视图,也就是两棵树合并为一棵树。

注意js的执行也是在主线程中进行, 所以需要注意引入js的时机和顺序,防止js阻塞DOMTree的形成。当然也可以采用async或者defer等方式异步加载js

image.png

DOM 与标记之间几乎是一一对应的关系。比如下面这段标记:

image.png

可翻译成如下的 DOM 树:

image.png

将 HTML 文档解析为 DOM 树的解析算法由两部分组成,此算法由两个阶段组成:标记化和树构建

www.whatwg.org/specs/web-a…

image.png

标记化

标记化是词法分析过程,将输入内容解析成多个标记。HTML 标记包括起始标记、结束标记、属性名称和属性值。

标记生成器识别标记,传递给树构造器,然后接受下一个字符以识别下一个标记;如此反复直到输入的结束。

树构建

第一个模式是 “initial mode” 。接收 HTML 标记后转为 “before html” 模式,并在这个模式下重新处理此标记。这样会创建一个 HTMLHtmlElement 元素,并将其附加到 Document 根对象上。

然后状态将改为 “before head” 。此时我们接收“head”标记。即使我们的示例中没有“head”标记,系统也会隐式创建一个 HTMLHeadElement,并将其添加到树中。

现在我们进入了 “in head” 模式,然后转入 “after head” 模式。系统对 body 标记进行重新处理,创建并插入 HTMLBodyElement,同时模式转变为 “in body”

现在,接收由“Hello world”字符串生成的一系列字符标记。接收第一个字符时会创建并插入“Text”节点,而其他字符也将附加到该节点。

接收 body 结束标记会触发 “after body” 模式。现在我们将接收 HTML 结束标记,然后进入 “after after body” 模式。接收到文件结束标记后,解析过程就此结束。

image.png

image.png

Style

主线程在获取DOMTree后还不知道页面长什么样子,所以需要进行CSS解析,读取CSS样式表,来形成CSSOM规则解析树。

image.png

style步骤依赖前置的DOM树解析结果,选择器是选择DOM节点集合决定最后应用范围,最后样式生效是多个选择器共同作用的结果,而且样式间可能互相冲突导致没有按照预期运行,关于选择器的优先级感兴趣的同学自行查阅

CSS解析器样式表StyleSheet构建样式规则。样式表可能位于<style>元素、单独加载的资源的css文件中,也可能由浏览器默认提供。样式规则以各种方式编制索引以实现高效查找。 属性类在构建时由Python脚本自动生成,以声明方式定义了所有样式属性,如上图右上侧css_properties.json经过py脚本转化为.cc文件。

ComputedStyle

样式的重新计算(recalc)从活动样式表中获取所有解析的样式规则,并计算每个DOM元素的每个样式属性的最终值。这些内容存储在一个名为ComputedStyle的对象中,该对象只是样式属性到值的映射。可以看到其实没一个DOM节点都对应有一个ComputedStyle对象

在Chrome浏览器里的话,就是对应开发者工具的Computed样式属性这一栏。或者是通过getComputedStyle的JSAPI去获取。

CSS 解析语法:CSS 规范定义了 CSS 的词法和语法

在 chromium 中解析 CSS 的代码

source.chromium.org/chromium/ch…

image.png

页面当中的 h1 就是比 h2 大。因为在浏览器中已经内置了默认的样式。

source.chromium.org/chromium/ch…

image.png

解析完 CSSOM 和 DOMTree,主线程通过遍历 DOM 和经过计算好的规则样式表,会结合形成 LayoutTree,也就是布局树,LayoutTree 上的每一个节点都记录了x,y坐标位置和边框尺寸。

Layout

在 DOM 和 Style 计算好后开始进入布局 Layout 阶段,比如将 DIV 解析为一个块级的LayoutRect 区域,用x+y+width+height来表示,布局就是为了计算x,y,width,height这些数据。

默认情况下文档按照顺序排列下去形成了文档流

文字和内联元素则是左右浮动的,而且内联元素会被行尾打断(自动换行)。当然也有从右到左的语言,比如阿拉伯语和希伯来语。

布局也包括字体的排列,因为布局需要考虑文本在那里进行换行,Layout 使用名为 HarfBuzz 的开源文本库来计算每个字形的大小和位置,这决定了文本的总宽度。字体成型必须考虑到排版特征,如字距调整letter-spacing和连字。

布局可以计算单个元素的多种边界矩形。例如,当存在溢出时,Layout 将同时计算边界框和布局溢出。如果节点的溢出是可滚动的,Layout 还会计算滚动边界并为滚动条预留空间。最常见的可滚动 DOM 节点是文档本身。

表格元素或display:table的样式需要更复杂的布局,这些元素或样式指定将内容分成多列,或浮动对象漂浮在一边,内容在其周围流动,或者东亚语言的文本垂直排列,而不是水平排列。

通过遍历 DOM 树创建渲染树LayoutTree,节点一一对应。布局树中的节点实现布局算法。根据所需的布局行为,LayoutObject有不同的子类。比如LayoutBlockFlow就是块级Flow的文档节点。样式更新阶段也构建布局树。

在样式解析最后结束时需要构建布局树LayoutTree,布局阶段遍历布局树,对布局树每个节点LayoutObject执行布局,计算几何数据、换行符,滚动条等。

DOM节点跟Layout节点不一定是一一对应

一般情况下一个DOM节点会有一个LayoutObject,但是有时候LayoutObject是没有DOM节点与之对应的。 比如上图,span标签外部没有div标签嵌套,但是LayoutTree会自动创建LayoutBlock的匿名节点与之对应,再比如样式有display:none的样式,那么也不会创建对应的LayoutTree

最后,如果是shadowTree的话,其LayoutObject节点可能会在不同的TreeScope

布局引擎正在重写

如上图所示,LayoutNG代表下一代的布局引擎,2020年布局引擎还在过渡阶段,所以有中间形态,如上图包含了LayoutObjectLayoutNGMixin混合节点。未来所有节点都会变成LayoutNGlayout object

NG节点的更新主要是因为之前的节点包含了输入、输出还有布局算法的信息,也就是说单个节点可以看到整棵树的状态(节点有可能需要获取父节点的宽高数据,但是父节点正在递归子节点布局中,实际上还没确定最后的布局)。

而新的NG节点对输入和输出做了明显的区分,而且输出是immutable的,可以缓存结果

布局结果指向描述物理几何的片段树,如图一个NGLayoutResult对应几个NGPhysicalFragment,对应右上角的几个矩形图形,如果NGLayoutResult没变化则对应整块都不会变化。

举一个布局例子

上图的HTML代码,会渲染如右下角的例子,对应的DOM树如左侧所示

DOM树跟Layout树很像,节点几乎是一一对应的,但是注意这里anonymous匿名节点被创建出来,它只有一个块级子元素。一个布局节点只能拥有块级元素或者内联元素其中之一

上图的子元素前面两个其实共享了匿名LayoutNGBlockFlow,也就是说有共同的父节点

fragment tree里我们可以更好的看到文本换行后的绘制结果,以及每个fragment的位置和大小

Paint

paint阶段只是创建绘制指令paint op,页面还没有东西,甚至直到GL调用之前页面都是没有呈现任何东西的状态

创建绘制指令列表

绘制 paint 阶段创建绘制指令列表paint ops list

绘制指令paint op可以理解为在某些坐标用什么颜色画一个矩形类似的意思。每个布局对象LayoutObejct可以有多个显示项目,对应于其视觉外观的不同部分,如背景、前景、轮廓等。

打开 Chrome 的 Layer 工具

image.png

1: document 的绘制列表

2: 拖动区域中的进度条可以重现列表的绘制过程

样式可以控制绘制顺序

正确的绘制顺序非常重要,这样当元素重叠时,它们才能正确堆叠。顺序可以由样式控制,而不是完全依靠DOM的先后顺序

绘制分不同阶段进行

每个绘制阶段paint phase都需单独遍历堆叠上下文staking context

一个元素甚至可能部分位于另一个元素的前面,部分位于另一个元素的后面。这是因为绘制在多个阶段中运行,每个绘制阶段都对自己的子树进行遍历。

举一个绘制例子🌰

如上,一个样式和 DOM 节点渲染出来的结果,包含了四个绘制指令 paint ops

  1. document背景色绘制
  2. 块级元素的背景色绘制
  3. 块级元素的前景色绘制(包含文本的绘制)

文本块的绘制

文本绘制操作包含文本块的绘制,其中包含每个字的字符和偏移量以及字体。如图这些数据都是 HarfBuzz 计算后得到的

Raster

中文说的栅格化或者光栅化,取自 PS 图层右键的栅格化为译文。熟悉 PS 的会知道矢量图形栅格化后,放大图形会“糊”。但是不做栅格化处理直接放大矢量图形则不会。原因就是栅格化后只记录了单像素点的 rgba 值,放大后本来一个点数据,要填满N个点,图像就”糊了“。

Raster 将绘制指令转化为位图

把绘制列表里的绘制操作执行过程,称为任务,或称栅格化。比如 PS 里的图层任务,主要区别就是本来矢量的图,栅格化后会变成位图 bitmap,后面再缩放就会模糊。

生成的位图 bitmap 中的每个单元格都包含对单个像素的颜色和透明度进行编码的位。这里用十六进制表示一个点的 rgba 值。

Raster 还可以对页面图片进行解码

栅格化还可以对页面中嵌入的图像资源进行解码。绘制指令引用压缩数据(JPEG、PNG等),栅格化会调用适当的解码器将其解压缩。

GPU加速栅格化

GPU 可以运行生成位图的命令(“加速栅格化”)。请注意,这些像素还没有出现在屏幕上! raster 产生的位图数据存储在 GPU 内存中,通常是 OpenGL 纹理对象引用的 GPU 内存。

过去通常是存在内存里再传给 GPU,但是现代 GPU 可以直接运行着色器 shader 并在 GPU 上生成像素,这种情况称为“加速栅格化”。但是两个结果都是一致的,最终内存(主存或者GPU内存)里拥有位图 bitmap

Raster 通过 Skia 发出 GL 调用

GL 调用即 OpenGL 调用,OpenGL 意为"开放图形库",可以在不同操作系统、不同编程语言间适配2D,3D矢量图的渲染。

image.png

raster 通过名为 Skia 的库发出 GL 调用。Skia 提供了围绕硬件的抽象层,如路径和贝塞尔曲线,子像素抗锯齿以及各种混合叠加模式。

Skia 是开源的,由谷歌维护。跟随 Chrome 一起发布,但位于单独的代码库中。它也被其他产品使用,比如 AndroidSkiaGPU 加速代码路径构建自己的绘制操作缓冲区,在栅格化结束时刷新。实际上发起 GL 调用的是 Skia 的后端。

Raster运行在GPU进程中

回想一下,渲染器进程是一个沙箱环境,因此它不能直接进行系统调用。绘制操作被运送到GPU进程进行任务处理。GPU进程可以发出实际的GL调用。除了独立于渲染器沙箱之外,在GPU进程中隔离图形化操作还可以保护我们免受不稳定或不安全的图形驱动程序的影响。比如GPU进程崩了,浏览器可以重启GPU进程

绘制请求通过命令缓冲区传递给GPU进程

栅格化的绘制操作通过GPU命令缓冲区command buffer传输,渲染进程和GPU进程通过IPC通道发送。命令缓冲区command buffer最初是为序列化的GL图形命令构建的,类似一个proxy。当前的“进程外”栅格化(即GPU)以不同的方式使用它们,更多是绘制操作的包装器,就是命令缓冲区command buffer与底层图形API无关

不同操作系统调用不同的共享OpenGL库

GPU进程中的GL函数指针通过动态查找操作系统底层共享的OpenGL库进行初始化,Windows上用ANGLE做一个转化步骤。

Angel是另一个由Google构建的库;它的工作是将OpenGL转换为DirectX,DirectX是微软在Windows上用于加速图形的API。调查发现Angle比Windows的OpenGL驱动程序运行更好。

至此拥有了整个流水线,从 DOM 一直到内存中的像素,牢记渲染不是静态的,也不是执行一次就完成了,浏览器会话期间发生的任何事情都会动态更改渲染的过程。并且整个流水线从头开始运行是非常昂贵的,尽可能能减少不必要的工作以提高效率。

frames动画帧

低于60帧每秒的动画和滚动看起来会非常卡,渲染器生成动画帧,每个帧都是内容在特定时间点状态的完整呈现,多个帧连起来就是看到的动画,其实动画只要达到60帧每秒,那么看起来就会是连贯的。新的设备甚至要求90或120甚至更高的帧率。

如果在1/60秒内,约16.66ms还不能渲染完一帧画面,那么画面看起来就是断断续续很卡的样子。

流水线各个阶段都依赖上一步的结果

为了提高性能,尽可能复用上一阶段处理的结果。

重绘

大块区域的绘制和栅格化是非常昂贵的,比如在滚动的时候,视口内所有像素都变化了,这个过程称为重绘repaint。

渲染进程主线程的竞争关系

渲染进程主线程的任何事情都会跟JS竞争(互斥关系),意味着其实JS也会阻塞渲染主线程其他任务的执行。

分层与合成线程

将页面分解为不同的层便于栅格化 raster 对不同的层单独处理,在渲染进程的主线程构建层后会commit到合成线程(compositor thread)去,合成线程(compositor thread)会对每一层进行单独绘制。

分层的目的是可以对单独的层进行变换transform和栅格化 raster

试想一下如果有123三层,其中1,2两层没变化,第3层旋转了,那么只要对第三层每帧进行变换就可以得到每一帧的输出,计算量大大减少。

所以分层的目的是为了减少计算加速渲染效率,在渲染进程的合成器线程执行则是为了不影响渲染主线程的任务执行

图中的 impl* 即渲染进程的合成线程,因为历史原因在代码里都是这样表示,后面所有表示合成线程都用impl表示。

为什么要分层?

分层的作用在有动画时候可以显著提升性能,如图所示BBB文本一层的变换不会影响其他层

动画是层的移动,页面滚动是层的移动和裁剪,放大缩小也是层的缩放

合成线程处理输入

当滚动事件没有触发JS逻辑时候,即使渲染进程的主线程很繁忙,但是浏览器进程发出的页面滚动事件的处理也不会受到影响,因为渲染进程的合成线程compositor thread可以单独处理滚动事件。

当然如果滚动触发了JS的逻辑,那么合成线程必须转发事件到主线程去,滚动事件会进入主线程任务队列等待处理。

层的提升

正常情况下一个 LayoutView 会创建一个 PaintLayer,对应一个cc(Chromium Compositor) Layer。 但是某些样式属性也会导致对应的 LayoutObject 单独成层,比如 transform 属性就类似创建新层的“触发器”一样,浏览器遇到这个属性就会单独创建新层,cc(Chromium Compositor) Layer 没有父子关系,是一个平级的列表,但是还是保留 LayerTree 的名称。

滚动容器创建多个层

滚动容器创建特殊的多个层,比如元素加了overflow:scroll的滚动属性,那么合成的时候会有5个层,其中4个层都是滚动条scrollbar的层,这些层合并起来称为CompositedLayerMapping

合成透明滚动条会禁用子像素抗锯齿,如上图左下角所示。而且判断是否合成滚动条也有判断逻辑,在安卓和ChromeOS上可以合成所有的滚动条

合成任务

如上图,合成任务包含构建层树的过程。在布局layout之后,绘制paint任务之前,这个过程也可以称为分层和合成任务,每一层layer都是独立绘制的,一些属性节点单独为层,比如will-change,3D属性transform之类

prepaint构建属性树

属性树

渲染进程合成线程绘制的时候,合成线程里的合成器可以将各种属性应用于其绘制的图层,如变换矩阵,裁剪,滚动偏移,透明度。这些数据储存在属性树里,可以将这些理解为图层的属性(过去也是这么干的)。后面为了解耦这些属性,让它们可以脱离层单独使用,需要引入prepaint的过程

预绘制prepaint阶段遍历并构建属性树

合成后绘制(CAP)

CAP是composite after paint的缩写,它的目标是将属性和层解耦。即在paint阶段只需要paint的信息,而不需要知道层的任何信息,因为这时层还没有构建

在过去,变换、裁剪、效果滚动等信息等存储在层本身上,但CAP要求层的属性解耦。未来,层layer的合成会在绘制后进行

commit复制层数据到合成线程

在绘制paint阶段完成后,即绘制指令准备完成后,会进入渲染进程合成线程commit阶段

commit会拷贝层和属性树生成副本,这里合成线程的commit会阻塞主线程直到commit完成

注意:渲染进程合成线程拿到的是layer副本,用LayerImpl表示

tiling分块平铺

整个网页是非常大的,向下延伸理论上可以无限长(比如新闻类网站的无限滚动)。

栅格化是绘制之后的步骤,栅格化会将绘制指令转化为位图bitmap。试想一下如果在绘制完整个图层之后再栅格化整个图层,则成本会很大,但如果只栅格化部分图层的可见部分成本则会小很多。

这里tiling是平铺的意思,类似装修时候铺地板用大块瓷砖平铺,页面显示的做法类似。

根据视口viewport所在位置的不同,渲染进程合成器线程会选择靠近视口的图块tiles进行渲染,将最后选择渲染的图块传递给GPU栅格化线程池里的单个栅格化线程执行栅格化,最后得到栅格化好后的tile图块。图块大小根据不同设备的分辨率有不同的大小,比如256*256512*512

图块tiles是栅格化任务的单位,栅格化就是将一块块的tiles转化为位图bitmap

根据分块tiles去绘制层

在栅格化所有的图块tiles完成后,渲染进程的合成器线程会生成draw quads命令。

quad类似于在屏幕上特定位置绘制图块tile的指令,draw quads就是绘制图块们的意思。

此时的quad是层树layer tree在拿属性树经过一堆变换后的最终结果,每个quad都引用图块tile在GPU内存里的栅格化输出结果。

多个DrawQuad最后被包装在CompositorFrame里(简单理解就是一排要铺上去的瓷砖),这是渲染进程最后的输出,包含有渲染进程生成的动画帧,会被传递给GPU进程。

注意执行到这里还只是数据,这里屏幕还没有像素呈现

activation

在准备图块tiles进行栅格化和draw两个阶段渲染进程的合成线程都会参与,但是渲染进程主线程里的layer数据还在不断commit过来。实际上合成线程具有两个树的拷贝副本

  • pending tree: 负责接收新的commit并转给栅格化线程池里的栅格化线程执行,完成后进入激活activation阶段,同步复制处理好后的layer副本到active tree里
  • active tree: 绘制上一次activation同步复制的layer副本(来自上一个commit)

这里pending treeactive tree都是层列表和属性树的结合,不是真的树结构,基于习惯沿用树的叫法

display(Viz)

GPU进程的显示合成器display compositor会将多个进程最后的CompositorFrame进行合并显示,前面说过CompositorFrame是每个进程最后的输出,包裹了DrawQuad列表。

可以看到这里也有浏览器主进程的CompositorFrame,导航栏,收藏夹,前进后退这些Content外的渲染是浏览器主进程控制的。浏览器主进程有自己合成器为浏览器UI生成动画帧,比如标签条和地址栏的动画。

界面可以嵌入其他界面。浏览器嵌入渲染器,渲染器可以嵌入其他渲染器用于跨源iframe(也称为站点隔离,“进程外iframe”或OOPIF)。同源网页,比如iframe和一个标签页可能共用一个渲染进程,而跨源网页则一定是多个渲染进程。

显示合成器display compositor在GPU进程中的Viz线程上运行。Viz取Visuals视觉效果的意思。

显示合成器display compositor同步传入的帧,了解嵌入界面之间的依赖关系,做界面聚合。

Skia

Viz线程除了做界面聚合还发起图形调用,最后屏幕上显示compositor frame的quad。Viz线程是双缓冲的,分为前置缓冲区和后置缓冲区,这里将数据处理后序列化放到后置缓冲区。

旧模式是GPU主线程解码器真正发起GL调用,新模式中是交给Skia库。Skia绘制到一个异步显示列表里,会一起传递到GPU主线程。GPU主线程的Skia后端发起真正的GL调用。

分离GL调用通过第三方的Skia或者未来准备使用的Vulkan实现与OpenGL解耦

前后缓冲区

在大多数平台上,显示合成器display compositor的输出是双缓冲的,即包含前后两个缓冲区。图块绘制到后台缓冲区,Viz发出命令交换前后缓冲区使其可见

也就是说屏幕显示器这一帧的画面,是每HZ从前置缓冲区读取后在屏幕显示的,后置缓冲区在马不停歇的绘制,通过前后缓冲区的交换实现新一帧画面的呈现。在OS X上,使用CoreAnimation做了一些稍微不同的事情

显卡的作用?负责将数据写到后缓冲区,写完后前后缓冲区互换。通常情况下显卡的更新频率和显示器的刷新频率是一致的,如果不一致则会发现视觉上的卡顿。大多数设备屏幕的更新频率是60次/秒,这也就意味着正常情况下要实现流畅的动画效果,渲染引擎需要每秒更新60张图片到显卡的后缓冲区

至此浏览器完成了它的任务,底层驱动通过调用硬件完成绘制。最后,我们的像素出现在屏幕上

渲染流水线

回顾一下整个渲染流水线的过程,从渲染主线程获取Web内容,构建DOM树,解析样式,更新布局,layer分层后合成,生成属性树,创建绘制指令列表。

再到渲染进程合成线程收到渲染主线程commit过来的带有绘制指令和属性树的layer,将layer分块为图块,使用Skia对图块进行栅格化,拷贝pending treeactive tree,生成draw quads命令,将quad发送给GPU的Viz线程,最后像素显示到屏幕上。

大多数阶段是在渲染器进程里执行的,但是raster和display则在GPU进程中执行。

核心渲染阶段DOM,style,layout,paint是在渲染进程主线程力的Blink进行的,但是滚动和缩放等交互事件在渲染主线程繁忙时可以在渲染进程合成线程里执行

渲染进程主线程

  1. DOM: 解析HTML生成DOM树
  2. style: 解析styleSheet生成ComputedStyle
  3. layout: 生成 layout tree,跟 DOM 树基本对应,但是display:none的节点不显示,内联元素会创建 LayoutBlock 匿名节点包裹
  4. layer分层后合成: 某些样式属性会单独形成层,如transform会形成单独的层方便进行图形变换,滚动元素会多出scrollbar的4层。合成任务在渲染进程的合成线程中执行,与渲染主线程隔离互不影响
  5. prepaint: 为了将属性与层解耦引入prepaint阶段,prepaint阶段需要遍历并构建属性树,属性树即存储如变换矩阵,裁剪,滚动偏移,透明度等数据的地方,方便后面paint阶段拿属性树数据处理
  6. paint: 绘制过程是将LayoutObject转化为绘制指令paint op,每个LayoutObject会对应多个绘制指令paint ops,比如背景,前景,轮廓等。样式可以控制绘制的顺序。绘制有自己的顺序,如背景色在前,其次是浮动元素,前景色,轮廓 outline

渲染进程合成线程

页面的滚动等交互会进入渲染进程合成线程compositor thread里处理,这也是渲染进程主线程繁忙时交互也不卡的原因

  1. commit: 渲染进程合成线程将层从渲染主线程拷贝出两份层和属性树副本
  2. tiling: 栅格化整个图层成本大,渲染进程合成线程将layer分块后选择视口相近的图块tiles再进行栅格化成本小很多
  3. activate: 合成线程具有两个树的副本,pending tree负责将新commit的layer转到栅格化线程池里的栅格化线程处理好后同步到active tree
  4. draw: 栅格化所有变换后的图块之后,生成draw quads命令,包含多个DrawQuad的CompositorFrame,这是渲染进程最后的输出,此时屏幕还没有像素出现

GPU进程

  1. raster: 栅格化是将绘制指令paint op转化为位图bitmap的过程,转化后每个像素点的rgba都确定。栅格化还处理图片解码,通过调用不同解码器解压缩图片,GPU可以加速栅格化,通过调用Skia对图块进行栅格化
  2. Skia: 封装OpenGL调用,提供异步显示列表,最后传递到GPU主线程处理,GPU主线程的Skia后台发起真正的GL调用
  3. display: GPU Viz线程里的显示合成器display compositor合并多个进程的CompositorFrame输出,并通过Skia发起图形调用,像素呈现在屏幕上

参考文章: