❝万物皆有裂痕,那是光照进来的地方❞
大家好,我是「柒八九」。
本来呢,最近在规划一篇关于浏览器的文章,但是在做文章架构梳理和相关资料查询的时候,发现「浏览器在渲染页面」的过程中,也别有洞天。索性,就单独将其作为一篇文章来写。
这里有几点说明。
Chrome浏览器,所以下面的文章都是以Chrome浏览器为准。Chromium和Chrome可以认同是一个东西,不做强制区分,理由下文会讲到。还有,之前我们写过浏览器相关的知识点,如果想了解该系列文章(「浏览器相关」),可以参考我们已经发布的文章。如下是往期文章。
Chromium 和 Chrome的关系Chromium架构简析❞
好了,天不早了,干点正事哇。
Chromium和Chrome之间存在密切的关系,可以理解为Chromium是Chrome的开源项目。
Chromium:Chromium是一个开源的Web浏览器项目[1],由Google主导开发。它是一个完全开放的项目,源代码可以公开获取并进行自由修改。Chromium项目包括浏览器引擎Blink、JavaScript引擎V8等组件。 Chromium致力于提供一个可扩展、快速和安全的Web浏览器解决方案,同时也是许多其他基于Chromium的浏览器的基础。Chrome:Chrome是由Google基于Chromium项目开发的Web浏览器。Chromium的商业版本,针对普通用户提供了更多功能和服务。Chrome具有更多的集成功能,包括「自动更新」、「PDF阅读器」、Google账号同步等。Chrome还包括一些针对企业用户和开发人员的工具和功能。❝可以将
Chromium视为Chrome的基础,Chrome在此基础上添加了自己的功能和服务。 ❞
想必大家都有Chrome浏览器,我们可以做一个验证,大家在地址栏中输入chrome://settings/help或者按照如下的步骤。

关于Chorme和Chromium的关系就映入眼帘。如下图所示。

关于它们之间的关系,我们就不在赘述。
其实Chromium也是可以被下载,同时也可以作为搜索引擎的。

市面上,很多浏览器都是基于Chromium浏览器开发的。
Edge 浏览器

360浏览器

Chromium架构简析Chromium被分成两个主要部分(不包括其他库):浏览器Browser和渲染器Renderer(包括Blink,网络引擎)。
用户界面和I/O用户界面并管理渲染器Renderer和其他进程"浏览器进程"或简称为浏览器BrowserBlink开源布局引擎来解释和布局HTML。它们的关系如下

Chromium
每个框代表一个应用层。任何一个低层级都不依赖于更高层级的内容。
我们按照从底层到顶层的顺序,来简单介绍下,每个层级的作用。
WebKit:在Safari、Chromium和其他基于WebKit的浏览器的渲染引擎。Port)是WebKit的一部分,它与平台相关的系统服务(如资源加载和图形生成)进行集成。Glue:将WebKit的数据类型转换为Chromium的数据类型的组件。"WebKit嵌入层"Chromium的基础。Renderer / Render host:这是Chromium的"多进程嵌入层"。代理通知和命令发送。WebContents:是Content模块的组件。Tab Helpers:附加到WebContents上的「单独对象」。助手对象附加到它所持有的WebContents上(如网站图标、信息栏等)。将上面的比较生硬的词汇替换一下,然后就可以画出下面的关于Chromium架构图。

❝这块的架构图,有些生硬,后期也会有专门的文章来进行讲解。 ❞
通过上文介绍,我们得到一个结论 :Chromium ≈ chrome。 所以,下文中可以将Chromium和chrome看成一个东西。
下图是chrome将content生成页面信息的示意图。


❝在
Chromium C++代码库中,在架构层面上content负责「红色框」中的所有内容。(可以看上面的架构图) 而Tabs、地址栏、导航按钮、菜单等不在content的范围内。 ❞
Chrome安全模型的关键是「渲染发生在沙盒化的进程中」。
Blink是渲染器Renderer进程中的「代码子集」,在content命名空间内。Blink实现了Web平台API和Web规范的语义。渲染器Renderer进程还运行一个称为合成器compositor("cc")的组件。
对应的关系如下:(从进程和线程的关系角度看)

❝
content是Chromium中用于表示网页内部或 Web 应用程序前端的所有代码的通用术语。 也就是在上面架构图中的content❞
常见的类型包括文本、图像、HTML元素(包围文本的标记语言)、CSS(定义HTML元素的表现方式)和JavaScript(可以「动态修改上述所有内容」)。

除了上述列举的常见的内容类型,像<video>, <canvas>, WebAssembly, WebGL, WebVR, PDF也属于Content的范畴。
如果对WebAssembly不了解,可以翻看之前写的 -浏览器第四种语言-WebAssembly。
还有关于WebGL也打算写相关系列的文章,敬请期待.....
我们通过一个真实的案例来看一下。下图是最近很🔥的ChatGPT的地址。左侧是真实的页面显示,右侧是该页面中包含的「内容信息」。

❝可以看到「一个真实的网页是由数千行HTML、CSS和JavaScript代码的纯文本形式的所组成」 网页的「源代码是渲染器Renderer的输入」 ❞
❝像素Pixels是「图像的最小单位,它是构成数字图像的基本元素」。 ❞
"像素"一词源自于"picture element"的缩写。每个像素代表了图像中的一个点,它具有「特定的位置和颜色信息」。
在计算机图形中,像素Pixels通常被表示为一个「二维矩阵或数组」,它们排列在网格中,形成图像的整体。每个像素可以存储图像的亮度、颜色和透明度等信息。对于「彩色图像」,通常使用「RGB(红、绿、蓝)模型」来表示每个像素的颜色,其中每个分量的取值范围通常是0到255之间。
像素Pixels的「密度」决定了图像的清晰度和细节水平。更高的像素密度意味着在给定的显示区域内有更多的像素,从而能够呈现更多的细节。常见的像素密度单位是「每英寸像素数」,称为PPI(Pixels Per Inch)。
在计算机图形处理中,我们可以通过「操作和改变像素的颜色、位置和透明度来实现图像的绘制、编辑和处理」。像素在计算机图形、摄影、显示技术和计算机视觉等领域起着至关重要的作用,它们「是数字图像的基本组成部分」。
表示方式 | 示例 | 描述 |
|---|---|---|
十六进制表示法 | #FF0000 | 使用六位十六进制数表示颜色,每两位表示红、绿、蓝三个通道的亮度值,取值范围是00到FF。 |
RGB表示法 | rgb(255, 0, 0) | 使用RGB值表示颜色 |
RGBA表示法 | rgba(255, 0, 0, 0.5) | 使用RGB值和Alpha通道表示颜色Alpha通道的取值范围是0.0到1.0,0.0表示完全透明,1.0表示完全不透明 |
HSL表示法 | hsl(0, 100%, 50%) | 使用色相(Hue)、饱和度(Saturation)和亮度(Lightness)来表示颜色。色相的取值范围是0到360,饱和度和亮度的取值范围是0%到100% |
HSLA表示法 | hsla(0, 100%, 50%, 0.5) | 与HSL表示法类似,增加了一个Alpha通道来表示透明度,取值范围也是0.0到1.0 |

❝
渲染过程可以被描述为:将HTML/CSS/JavaScript等数据类型进行转换,并且输入到OpenGL以被调用,以显示像素。 ❞
同时,在Chrome渲染过程中,我们还希望获得正确的「中间数据结构」,以便快速响应之后的「更新操作」,并能够快速响应JS等的数据查询。
可以将渲染过程分为多个"生命周期阶段",生成这些中间输出。

在之前的「计算机底层知识」系列中,我们讲过计算机CPU能直接解释运行的只有「本地代码」(机器语言)程序。用JS/Java等高级语言编写的源代码,需要通过各自的「编译器」编译后,转换成本地代码。 (有兴趣的可以翻看之前的文章)。下面的处理过程也是类似的。大家可以进行类推分析。
像HTML/CSS/JS是不能够被浏览器直接识别的,是需要进行「格式转换和处理」。这里就涉及到编译原理相关的知识点。(后期有打算,写相关的编译原理的文章,我们这里就不展开说明了)
❝
HTML标签通过「语意化处理」将网页进行了分层处理。 ❞
例如,一个 <div> 可能包含两个<p>,每个<p>都带有文本信息。因此,第一步是解析这些标签,构建一个反映这种结构的文档对象模型Document Object Model(简称:DOM)。

文档对象模型Document Object Model是一种用于表示和操作HTML、XML和XHTML文档的「编程接口」。它将文档解析为一个由节点Node和对象Object组成的「树形结构」,这个树形结构被称为DOM树。
DOM树的根节点是文档节点Document Node,它代表整个文档。文档节点下方是元素节点Element Node,表示HTML或XML文档中的标签。元素节点可以包含其他元素节点、文本节点Text Node、注释节点Comment Node等。
每个节点在DOM中都有「特定的属性和方法」,可以用于访问和操作节点的内容、属性和样式。一些常见的节点类型包括:
HTML或XML文档中的标签,如 <div>、<p>、<a>等。<!--开头和-->结尾。DOM提供了一组API,可以通过这些API来操作和修改DOM树。开发人员可以使用JavaScript或其他支持DOM的编程语言来访问和操作DOM。
❝通过DOM,我们可以「动态地创建、修改、删除和查询文档的元素和内容,从而实现动态的Web页面交互和数据操作」。 ❞
❝一个文档对象模型Document Object Model反映了「包含关系」。 ❞
在文档对象模型Document Object Model中,「每个HTML元素被表示为一个对象」,这些对象之间通过「父子关系」来表示它们之间的包含关系。
例如,如果有一个包含两个段落的 <div> 元素,那么在DOM中,将会有一个「表示 <div> 的对象,它包含两个表示段落的子对象」。这样的层次结构可以通过「递归方式」表示整个文档的层次关系。

❝
DOM具有双重功能,既作为页面的内部表示,又作为供JS查询或修改渲染的API。 ❞

JavaScript引擎(V8)通过一种称为绑定Bindings的系统,将DOM Web API暴露给开发者。
❝
JavaScript引擎(V8)通过绑定Bindings机制将JavaScript与底层的DOM接口进行连接。 这种机制允许开发者使用JavaScript来操作和操纵Web页面上的元素、样式、事件等。 实际上,这些DOM Web API只是对底层DOM树的操作进行了封装,提供了一种更便捷和直观的方式来与DOM进行交互。 ❞
❝在同一个文档中可能会存在多个
DOM树。 ❞

如上图所示,当我们使用自定义元素,在开启影子模式时,attchShadow({mode:'open'})就会产生多个DOM树。(如果对自定义元素的使用方式不是很明确的同学,可以参考这篇文章[3])
宿主节点的子元素(在宿主树中)被分配到影子树中的<slot>中。

FlatTreeTraversal从宿主节点向下遍历直至影子节点,同时将<slot>替换为指定的元素。
构建完DOM树之后,下一步是处理CSS样式。「CSS选择器用于选择DOM元素的子集,以对其添加指定的属性声明」。

❝在处理CSS样式时,浏览器会解析
CSS文件或内联样式,并将样式规则应用于DOM树中的相应元素。 CSS选择器用于选择要应用样式的目标元素。 选择器可以根据元素的标签名、类名、ID、属性等进行匹配,以确定应用哪些样式规则。 ❞
这里多啰嗦几句,在CSS重点概念精讲中我们介绍过,选择器。
这里我直接就拿来主义了。
.#[]:::)5个瞄准目标元素
.开头#开头[]的选择器[title]{}/[title="test"]{}:)的选择器:link :选择未被访问的链接:visited:选取已被访问的链接:active:选择活动链接:hover :鼠标指针浮动在上面的元素::)的选择器::before : 选择器在被选元素的内容前面插入内容::after : 选择器在被选元素的内容后面插入内容空格>~+)4个「根据与其他元素的关系」选择元素的选择器
>链接~链接+链接❝
!important (10000)1000)0100)0010)0001)❞

上面的优先级计算规则,内联样式的优先级最高,如果外部样式需要覆盖内联样式,就需要使用!important
例如,对于以下CSS规则:
h1 {
color: blue;
}
.my-class {
font-size: 16px;
}
第一个规则选择所有的 <h1> 元素,并将其文本颜色设置为蓝色。第二个规则选择具有类名为 my-class 的元素,并将其字体大小设置为16像素。
❝在应用CSS样式时,浏览器会「遍历DOM树,匹配元素与选择器,并将相应的样式属性应用于匹配的元素」。这样,每个元素都会根据匹配的CSS规则来设置其样式属性,从而实现页面的外观和布局。 ❞
通过处理CSS样式,我们可以为网页提供丰富的外观效果、布局和交互特性,使网页更加美观和易于使用。
CSS解析器CSS Parser会解析所有可达有效的样式表,包括内联样式表( <style>)、外部样式表(styles.css)和浏览器默认样式表。它会将样式规则解析为一个模型(这就是我们常说的CSSOM),其中「包含选择器和对应的样式声明」。

❝
选择器描述了要应用样式的目标元素样式声明定义了要应用的具体样式属性和值。
解析后的CSSOM包含了这些选择器和声明的组合。❞
为了提高样式规则的查找效率,CSS解析器CSS Parser会对样式规则进行「索引」。这样可以快速定位匹配特定选择器的样式规则,而不需要遍历整个样式表。
此外,属性类是在构建时由Python脚本自动生成的。属性类用于在运行时快速查找具有相同样式属性的元素。它们被用作索引的一部分,以便在应用样式时能够高效地定位和处理相同属性的元素。
总而言之,CSS解析器根据活动样式表构建样式规则模型,并通过索引和属性类来优化样式的查找和应用过程。这样可以提高渲染效率,并确保正确地应用样式到文档的各个元素上。

在样式解析(或重新计算)过程中,解析器会遍历DOM树中的每个元素,并根据匹配的样式规则计算出每个元素的样式属性的最终值。这些最终值包括继承的值、层叠的值以及通过CSS属性值计算得到的值。
所有计算得到的样式属性值会被存储在 ComputedStyle 对象中。这个对象可以被认为是一个巨大的「映射」,其中样式属性(如颜色、字体大小、边距等)与其对应的值关联起来。通过查询 ComputedStyle 对象,可以快速获取每个元素的最终样式属性值。
通过样式解析和计算,浏览器可以确定每个元素应用的最终样式,从而实现正确的页面渲染和布局。ComputedStyle 对象在渲染过程中起着重要的作用,为每个元素提供了其最终的样式属性值。
我们可以通过Chrome开发者工具可以显示任何DOM元素的ComputedStyle。

也可以通过JavaScript访问,getComputedStyle 是一个用于获取元素计算后的样式的方法。
document.styleSheets:所有的样式表(style sheets)。document.styleSheets返回的样式表集合来访问和操作具体的样式表。window.getComputedStyle(element):指定元素的计算样式(computed style)。getComputedStyle方法来获取该元素应用的最终样式。document.styleSheets[i].cssRules:cssRules属性返回的规则集合来访问和操作具体的样式规则。element.style:element.style来访问和修改元素的样式属性。使用 getComputedStyle 的基本语法如下:
var element = document.getElementById("elementId");
var computedStyle = window.getComputedStyle(element);
在上面的代码中,我们首先通过 document.getElementById 方法获取到一个具体的元素,并将其赋值给 element 变量。然后,我们使用 window.getComputedStyle 方法来获取该元素的计算后样式,将其赋值给 computedStyle 变量。
通过 getComputedStyle 获取到的样式是一个 CSSStyleDeclaration 对象,它包含了该元素所有计算后的样式属性和对应的值。
你可以通过以下方式来获取具体的样式属性的值:
var value = computedStyle.getPropertyValue("property");
其实在浏览器中,一共支持四种语言。

针对JS的处理,需要用到V8等引擎,WebAssembly也是需要做处理的。因为,篇幅有限,这里就不展开描述了。
针对JS的解析过程,可以参考JS执行流程
关于WebAssembly的介绍,可以参考浏览器第四种语言-WebAssembly
❝在构建完DOM并计算所有样式后,下一步是「确定所有元素的视觉几何属性」。 ❞
对于块级元素,我们需要计算一个矩形的坐标,该矩形对应于「元素所占据的内容的几何区域」。

对于前端页面元素而言,一个元素的类型可以隶属于不同的类型。但是,在比较宏观的角度看,元素是否占一行还是可以和文本信息同行显示。可以把元素分成「块元素」和「内联元素」。
在最简单的情况下,布局「按照DOM的顺序,从上到下,依次放置」。我们称之为「block flow」。(单独占一行)

文本节点和类似<span>的内联元素生成内联框inline boxes,通常「在一行中从左到右流动」。

而从右到左的内联流动方向则适用于RTL语言,如阿拉伯语和希伯来语。

布局Layout需要使用ComputedStyle 对象中的字体font信息来测量文本。 (这里再重申一下,ComputedStyle是CSS被解析后的对象)

❝布局Layout使用名为
HarfBuzz的「文本整形库」来计算每个字形的大小和位置,从而确定文本段的整体宽度。 ❞
❝布局Layout可能会为单个元素计算多种类型的矩形边界。 ❞
例如,在出现「溢出」情况时,布局会计算边框框盒border box rect和布局溢出框盒layout overflow rect。

如果节点的溢出是可滚动的,布局还会计算滚动边界scroll boundaries并保留滚动条的空间。
最常见的可滚动DOM节点是文档本身,它是树的根节点。
❝布局对象的内容可以超出其边框框盒border box rect。 ❞

同时,我们还可以设置如何处理超出部分的行为。
overflow:'auto'|'visible'|'hidden';
(是不是很熟悉)
对于表格元素或指定将内容分成多列的样式,或者浮动对象使内容围绕其一侧流动,需要更复杂的布局。

但是,不管布局如何复杂,在「布局」阶段,有一个亘古不变的规则就是: DOM结构和计算样式值(ComputedStyle)是布局Layout算法的输入
❝「每个流水线阶段都使用前一个阶段的结果」。 ❞
布局Layout在与DOM链接的单独树(布局树)上进行操作。(也就是说DOM树和Layout树有关联,但是不是一个树)
布局树Layout Tree中的节点实现了布局算法。
根据所需的布局行为,有不同的LayoutObject子类。样式更新阶段也会构建布局树。
❝布局阶段遍历布局树,对每个
LayoutObject执行布局操作。 ❞

通常情况下,一个DOM节点对应一个LayoutObject。但有时候,一个LayoutObject没有对应的DOM节点。
甚至有可能一个节点有多个LayoutObject(例如,一个内联元素在块级子元素内,并且内联元素之前和之后都有文本)。可以参考下图中<span>inline</span>的布局对象。

最后,布局树的构建基于FlatTreeTraversal(FlatTreeTraversal在解析DOM的时候,当存在多个DOM树的时候,出现过哈),可以跨越影子DOM边界。
布局引擎正在进行重写。布局树包含了传统布局对象和NG布局对象的混合。最终,所有的布局对象将会是NG布局对象。

在NG中,布局的输入和输出被清晰地分离开来。「输出是一个不可变的、可缓存的布局结果」。

❝NG布局结果NGLayoutResult指向描述物理几何结构的片段树Fragments Tree。 ❞

存在如下的页面结构。
<div style="max-width: 100px">
<div style="float: left; padding: 1ex">F</div>
<br>The <b>quick brown</b> fox
<div style="margin: -60px 0 0 80px">jumps</div>
</div>
最后的呈现效果。


❝布局对象Layout Object大多数情况下与DOM元素一对一对应。 ❞
但是,在Layout树中也会存在anonymous布局对象,它是为了「使其容器只包含块级子元素而创建的」。
布局块LayoutBlock可以具有块级子元素或内联子元素,但不能同时具有两者。
虽然,文本"quick brown" 存在断行情况,但是它是存在一个LayoutText节点中。

在片段树Fragment Tree中,我们可以看到「断行的结果」以及每个片段的「位置和大小」。




通过上述的数据处理,我们已经获取到布局对象Layout Object的几何属性,接下来我们就需要将其绘制处理了。
绘制记录Paint Records将「绘制操作」记录到显示项Display Items 列表中。
❝「绘制操作」可以是诸如"在这些坐标上以这种颜色绘制一个矩形"之类的内容。 ❞
对于每个布局对象Layout Object可能会有多个显示项Display Items,对应着其不同的「视觉呈现部分」,如背景、前景、轮廓等等。

这里多啰嗦几句,在CSS重点概念精讲中我们介绍过,关于层叠上下文和层叠顺序,这里我们只是做简单的知识介绍,如果想了解更多,可以参考之前的文章。
❝层叠顺序Stacking Order表示元素发生层叠时有着特定的「垂直显示顺序」 ❞
一旦普通元素具有层叠上下文,其层叠顺序就会变高
分两种情况
z-index数值,则其层叠顺序是z-index:autoz-index:0z-index数值,则其层叠顺序由z-index值决定
按正确的顺序绘制元素非常重要,这样它们在重叠时才能正确叠放。绘制顺序可以通过样式来控制。
❝绘制顺序是按照「层叠顺序」,而不是DOM顺序 ❞

可以看到,虽然yellow的DOM顺序在green的DOM之前,但是在绘制到页面上时,yellow在green的上面。(yellowZ轴大)
甚至有可能一个元素部分在另一个元素前面,部分在后面。这是因为绘制过程分为多个阶段,每个绘制阶段都会对子树单独遍历。
存在如下的页面结构:
<section class="container">
<div id="green"></div>
<div id="blue"></div>
</section>
对应的样式如下:
#green {
position: relative;
background-color: green;
height:200px;
width:300px;
}
#blue {
position: absolute;
top: 20px;
left: 30px;
width: 200px;
height: 100px;
background-color: blue;
border: 5px solid black;
}
#green::before {
content: "绿色元素的文案信息";
position: absolute;
top: 20px;
left: 10px;
z-index: 2;
color:white;
font-weight: 700;
}
在不考虑,CSS3其他特殊属性的情况下,当元素设置了z-index,就会生成一个层叠上下文,并且「每个绘制阶段都是对层叠上下文的单独遍历」。

存在如下的页面结果。
<style> #p {
position: absolute; padding: 2px;
width: 50px; height: 20px;
left: 25px; top: 25px;
border: 4px solid purple;
background-color: lightgrey;
} </style>
<div id=p> pixels </div>
呈现的效果如下:

两个DOM节点(包括#document)生成了三个显示项Display Items和四个「绘制操作」。

❝文本的
绘制操作包含一个包含「每个字形的标识符和偏移量」的块。 ❞
该步包含在显示项列表中,看上图中,位于最后一个.

❝显示列表Display List中的「绘制操作」通过称为光栅化Raster的过程来执行。 ❞

最后生成的位图中的每个像素单元都包含用于编码单个像素的颜色和透明度。
❝光栅化Raster还会解码嵌入在页面中的「图像资源」。 ❞

绘制操作引用了压缩数据(JPEG、PNG等),然后Raster调用相应的「解码器」对其进行解压缩。
❝渲染器进程是受沙盒保护的,因此它「无法直接进行系统调用」。 ❞
光栅化的绘制操作被封装在GPU命令缓冲区中,以便通过IPC通道发送。

光栅化通过一个名为Skia的库调用OpenGL。
Skia在硬件周围提供了一层抽象,并且能够理解更复杂的内容,如路径和贝塞尔曲线。
Skia是由Google维护的开源项目。它被集成在Chrome二进制文件中,但存在于一个单独的代码仓库中。
它还被其他产品(如Android操作系统)使用。Skia的GPU加速代码路径会构建自己的「绘图操作缓冲区」,在光栅化任务结束时进行刷新。

光栅化后的位图存储在内存中,通常是由OpenGL引用这些GPU内存。

❝GPU还可以执行生成位图的命令("加速光栅化")。 ❞
请注意,这些像素尚未显示在屏幕上!
绘制操作被发送到GPU进程进行光栅化。「GPU进程可以发出真正的OpenGL调用」。

上面所讲的流程从DOM=>style=>layout=>paint=>raster=>gpu是页面内容到「内存」中像素的全流程。但是,渲染过程不是静态的,而是需要「无时无刻」的将页面状态变化也要考虑进去。
所以,就又引入了我们下面的思考,页面状态发生变化该如何处理。

在讲变化前,我们再来介绍几个概念。
1000 / 60 ≈ 16ms。❝每个帧是内容在特定时间点的「完整渲染状态」。 ❞

❝图层Layer是页面的一部分,可以独立于其他图层进行变换和光栅化。 ❞

我们通过一个真实的案例,来看一下图层,并且它是如何被处理的。
有一个shake样式,它的作用是将指定的元素设置transform:rotate(xx)。让其可以实现在原本位置处,摆动。而这种情况,就是一个页面状态变化,是不能直接套用我们之前的渲染管道了。(我们之前的渲染管道是针对静态页面的)
.shake {
animation: shake 1s infinite;
transform-origin: center;
width:100px;
height:50px;
}
@keyframes shake {
0% {
transform: rotate(0);
}
50% {
transform: rotate(-22.5deg);
}
100% {
transform: rotate(22.5deg);
}
}
现在有如下的页面结构
<div>
AAA
<p class="shake"> BBB </p>
</div>
CCC
通过浏览器的处理后,可以发现现在有了一个单独的layer 区域。

而页面中呈现的效果如下。

页面结构如下:
<div class="shake">
AAA
<p > BBB </p>
</div>
CCC
layer结构:

页面呈现效果

❝「Compositing」 is the process or technique of combining visual elements from separate sources into single images, often to create the illusion that all those elements are parts of the same scene. -- 来自维基百科 ❞
翻译后的大概意思就是: 合成Compositing是「将来自不同来源的视觉元素组合成单一图像」的过程或技术,通常是为了创造所有这些元素是同一场景的一部分的错觉。
下面我们直接看看在页面中通过新增不同的动画效果而合成的视觉效果

通过移动构建的图层

通过滚动构建的视图

通过Zoom(缩放)构建的视图

某些样式属性会导致为布局对象创建一个图层。

「只有一些特殊的渲染层才会被提升为合成层」,通常来说有这些情况:
transform:3D变换:translate3d,translateZ;will-change:opacity | transform | filteropacity | transform | fliter 应用了过渡和动画(transition/animation)video、canvas、iframe其实,这里还和「层叠上下文」有牵扯,这里不展开说明了,具体可以参考CSS重点概念精讲

❝构建图层树Layer Tree是主线程上的一个新的生命周期阶段。 ❞
目前,在绘制Paint之前进行图层树的构建,并且每个图层单独进行绘制。

合成器可以对绘制图层的方式应用各种属性。这些属性存储在它们自己的树中。


在绘制完成后,提交(Commit)操作会在合成线程上更新图层列表和属性树的副本,以使其与主线程上的数据结构状态保持一致。


光栅化是在绘制之后的步骤,它将绘制操作转换为位图。图层可能很大,如果只有一部分可见,那么对整个图层进行光栅化既耗时间又没必要。
因此,合成线程将图层分割为瓦片Tiling。
❝瓦片是光栅化工作的单位。 ❞
瓦片使用专用的光栅化线程池进行光栅化。瓦片的优先级基于它们与视口Viewport的距离。
❝一旦所有
瓦片完成光栅化,合成线程将生成“绘制四边形”(Draw Quads)。 ❞
四边形类似于在屏幕上的特定位置绘制一个瓦片的命令,考虑了图层树应用的所有变换。每个四边形引用了内存中瓦片的光栅化输出。四边形被封装在一个合成器帧对象中,并提交给浏览器进程。

合成帧Compositor Frame来自多个渲染器和浏览器(浏览器有自己的用于 UI 的合成器)。
❝合成帧Compositor Frame与一个表面surfaces相关联,表示它们将显示在屏幕上的位置。 ❞
表面surfaces可以嵌入其他表面surfaces。
浏览器 UI 嵌入一个渲染器。渲染器可以嵌入其他渲染器,用于跨源 iframe(也称为站点隔离、"out of process iframe" 或 OOPIF)。
❝显示合成器在 GPU 进程的 Viz 合成器线程上运行。
viz 服务(简称为 "visuals")的一部分。❞
显示合成器接收传入的帧,并理解嵌入表面surfaces之间的依赖关系("surface aggregation")

Viz还会发出GL调用来显示合成帧Compositor Frame中的每个四边形。
这些GL调用在viz合成线程上,它们通过命令缓冲区进行序列化和代理,发送到GPU主线程,在那里解码器会发出真正的GL调用。

为什么要设置双缓存?解决画面撕裂!那何为画面撕裂呢?
屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即「每绘制完成一帧,显示器显示一帧」。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧的, 当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。
简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。
那咋解决画面撕裂呢?答案是使用 「双缓存」。
由于图像绘制和屏幕读取使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。
「双缓存,让绘制和显示器拥有各自的buffer」:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame/Front Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。如下图:

双缓存,CPU/GPU写数据到Back Buffer,显示器从Frame Buffer取数据
问题又来了:什么时候进行两个buffer的交换呢?
假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。
当扫描完一个屏幕后,设备需要「重新回到第一行」以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。那,这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing的状况。
VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。


「分享是一种态度」。
参考资料:
[1]
开源的Web浏览器项目: https://github.com/chromium/chromium
[2]
下载地址: https://www.chromium.org/getting-involved/download-chromium/#downloading-old-builds-of-chrome-chromium
[3]
这篇文章: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements
[4]
chromium结构: https://www.chromium.org/developers/how-tos/getting-around-the-chrome-source-code/