Mark24
浏览器渲染流程
渲染流水线
浏览器的渲染机制很复杂,渲染模块在执行的过程中会被划分为很多个子阶段,输入html经过这些子阶段,最后输出像素。我们把这个处理流程叫做“渲染流水线”。
大致表达如上图。
按照渲染的时间顺序,流水线可以划分为如下几个子阶段:
- 构建DOM树
- 样式计算
- 布局阶段
- 分层
- 绘制
- 分块
- 光栅化合成。
接下来在介绍每个阶段的过程中,你应该关注以下三个重点:
- 开始每个子阶段都有其 输入的内容
- 然后每个子阶段都有其 处理过程
- 最终每个子阶段都会生成 输出内容
构建DOM树
为什么要构建DOM树?因为浏览器无法直接理解和使用HTML,所以需要把HTML转换为浏览器能够理解的结构——DOM树。
另外打开Chrome开发者工具,选择console,控制台中输入“document” 回车,这样看到额就是完整的DOM树。
样式计算(Recalcuate Style)
为了计算出DOM节点每个元素的具体样式,这个阶段大概可分为散步
1. 把CSS转换为浏览器能够理解的结构
CSS的来源主要有三种
- 通过link引用的外部CSS文件
- 元素style属性内嵌的CSS
和HTML一样,浏览器无法直接理解这些纯文本CSS样式,所以渲染引擎接收到CSS文本时,会执行一个转换动作,将CSS文本转换为浏览器可以理解的结构—— StyleSheets。
Chrome控制台,输入 document.styleSheets
就可以看到。
这个样式表已经把那三种来源的样式都包含进去了。该结构同时具备了查询和修改的功能,这会为后面的央视操作提供基础。
2.转换像是表中的属性值,使其标准化
比如如下代码,其中有很多类型数值,不被浏览器理解,所以需要把所有值转换为渲染引擎理解的标准化的计算值。
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
标准化后为
body { font-size: 32px }
p {color: rgb(0,0,255);}
span {display: none}
div {font-weight: 700}
div p {color: rgb(0,128,0);}
div {color: rgb(255,0,0); }
可以看到一些值已经被替换。
3.计算出DOM树中每个节点的具体样式
样式属性已经被标准化,接下来需要计算DOM中每个节点的样式属性。
这就涉及到CSS的继承规则和层叠规则。
-
首先是CSS继承,CSS继承就是每个DOM节点都包含有父节点的样式。计算过程中,会根据DOM节点的继承关系来合理计算节点样式。
-
样式计算的第二个规则是样式层叠。 层叠是CSS的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在CSS处在核心的地位。CSS全称是“层叠样式表 正是强调了这一点。
布局阶段
我们有了DOM树、DOM树中元素的样式,但是依然无法显示,因为我们还不知道DOM的几何位置信息。
接下来就要计算DOM树中可见元素的几何位置——这个计算过程叫做“布局”。
Chrome在布局阶段需要完成两个任务
- 创建布局树
- 布局计算
1.创建布局树
DOM树还包含很多不可见元素,比如 head
标签,display:none
属性元素。所以在显示之前,我们需要额外的创建一颗只包含可见元素的布局树。
为了构建布局树,浏览器大概完成下面工作:
-
遍历DOM树中所有可见节点,并把这些节点加到布局树中
-
不可见节点会被忽略。比如head标签,比如 display:none的元素
2.布局计算
现在我们有了一颗完整的布局树,就要计算布局树节点的表坐标位置。布局计算的过程非常复杂。这里先跳过。后面再做详细介绍。
在执行布局操作的时候,会把布局计算的结果重新写回布局树,所以布局树既是输入内容,也是输出内容。这是布局阶段一个不合理的地方。针对这个,Chrome团队正在重构布局代码,下一代布局系统叫做LayoutNG。
中间总结
浏览器不能直接理解HTML数据,所以第⼀步需要将其转换为浏览器能够理解的DOM树结 构;⽣成DOM树后,还需要根据CSS样式表,来计算出DOM树所有节点的样式; 最后计算DOM元素的布局信息,使其都保存在布局树中。
分层
拥有了布局树,每个元素的具体位置都计算出来了,接下来可以绘制页面么?
还不行。
页面中有很多复杂的效果,比如一些3D变换,页面滚动或者z-index的z轴排序。为了方面实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并且生成一颗对应的图层树(LayerTree)。 使用过 PhotoShop,绘画软件应该很容易理解图层的概念。这些图层叠加在一起构成了最终的页面图像。
通常,并不是每个布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。最终每个节点都会直接或者间接的从属于一个层。
什么情况下会新建图层呢?
1. 拥有层叠上下文属性的元素会被提升为单独一层
页面是个二维平面,层叠上下文可以让HTML元素具有三维的概念,这些HTML按照自身属性的优先级分布在垂直于这个二维平面的z轴上
从图上可以看出,明确定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素,都拥有层叠上下文属性。
2. 需要裁剪(Clip)的地方也会被创建为图层
结合一下代码
<style>
div {
width:200;
height:200;
overflow:auto;
background: gray;
}
</style>
<body>
<div>
<p>所以元素有了层叠上下⽂的属性或者需要被剪裁,那么就会被提升成为单独⼀层,你可以参看下图:</p>
<p>从上图我们可以看到,document层上有A和B层,⽽B层之上⼜有两个图层。这些图层组织在⼀起也是⼀颗树状结
<p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树
</div>
</body>
在这⾥我们把div的⼤⼩限定为200 * 200像素,⽽div⾥⾯的⽂字内容⽐较多,⽂字所显示的 区域肯定会超出200 * 200的⾯积,这时候就产⽣了剪裁,渲染引擎会把裁剪⽂字内容的⼀部 分⽤于显示在div区域。出现这种裁剪情况的时候,渲染引擎会为⽂字部分单独创建⼀个层,如果出现滚动条,滚动 条也会被提升为单独的层。
所以说,元素有了层叠上下⽂的属性或者需要被剪裁,满⾜其中任意⼀点,就会被提升成为 单独⼀层。
图层绘制
在完成图层构建之后,渲染引擎会对图层树中每个图层进行绘制,接下来看渲染器引擎怎么实现图层绘制的?
试想一下,如果给你一张纸,让你先把纸背景涂成蓝色,然后在中间位置画一个红色圆,然后再在圆上画个绿色三角形,你会怎么操作?
通常你会把绘制过程分解为三部:
- 绘制蓝色背景
- 在中间绘制一个红色的圆
- 再在圆上绘制绿色三角形
渲染引擎在实现图层绘制的与这个类似,会把一个图层的绘制拆分成很多小的“绘制命令”,然后再把这些命令按照顺序组成一个待绘制列表。
也可以在开发者工具“Layers”(设置里可以勾选展示),选择“document”层,来实际体现下绘制列表。
栅格化(Raster) 操作
绘制列表只是用来标记绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎的合成线程来完成的,你可以接下下图来看下渲染主线程和合成线程之间的关系。
如图,当图层绘制列表准备好后,主线程会把该绘制列表提交(Commit)给合成线程,那么接下合成线程怎么工作呢?
先来看看新的概念,视口
视口(ViewPort)
把完整的网页想象成一张长长的图片,而我们看到的只是屏幕这个矩形框里面的一部分。屏幕上页面的可见区域,就叫做 视口(ViewPort).
有的图层可以很大,比如有的页面你需要滚动好久才能到底,但是通过视口,用户只能看到页面的很小一部分,所以这种情况下,要绘制出所有涂层的内容就会产生太大的开销,而且完全没有必要。
基于这个原因,合成线程会将图层划分为图块(tile),这些图快的大小通常是256x256或者512x512。
然后合成线程会按照视口附近的土块来优先合成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,就是将图块转化为位图。
而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。
通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫做快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。
GPU操作是运行在GPU进程中,如果栅格化操作使用了GPU那么最终生成位图的操作是在GPU中完成的,这就涉及到跨进程操作,可以参考下图。
从图中可以看出,渲染进程把生成图块指令发送给GPU然后在GPU中执行生成图块的位图并且保存在GPU的内存中。
合成(Composite)和显示
一旦所有图块都被栅格化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后讲该命令提交给浏览器进程。
浏览器进程里有一个叫做 viz的组件,用来接收合成线程发过来的 DrawQuad命令,然后根据DrawQuad命令,将其页面内容会知道内存中,最后再将内存显示在屏幕中。
经过这一些列的阶段,编好的HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面。
总结
-
渲染进程将HTML内容转换为能够读懂的DOM树结构。
-
渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。
-
创建布局树,并计算元素的布局信息。
-
对布局树进⾏分层,并⽣成分层树。
-
为每个图层⽣成绘制列表,并将其提交到合成线程。
-
合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
-
合成线程发送绘制图块命令DrawQuad给浏览器进程。
-
浏览器进程根据DrawQuad消息⽣成⻚⾯,并显示到显示器上。
相关概念
介绍完渲染流水线基础,再来看三个和渲染流水线相关的概念—— 重排、重绘、合成
1. 更新了元素几何属性(重排 Reflow)
如果你通过JavaScript或者CSS修改元素的几何位置属性,例如改变元素的宽高,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程叫做 重排Reflow。
无疑,重排需要更新完整的渲染流水线,所以开销是最大的。
2. 更新元素的绘制(重回 Repaint)
比如通过JS更改某些元素的背景颜色渲染流水线
图中可以看到,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为没有引起几何位置变换,所以直接进入绘制阶段,然后执行之后的一些列子阶段。
这个过程叫做 重绘。
相较于重排,重绘操作省去了布局和分层阶段,所以执行效率比重排操作高。
3. 直接合成(Composite)阶段
我们使用CSS的transform来实现动画效果,这就可以避开重排和重绘阶段。直接在非主线程上执行合成动画操作。这样效率是最高的。因为是在非主线程上合成的,并没有占用主线程的资源,另外也避开了布局、绘制两个子阶段。