上篇文章中,阐述了渲染流水线中的DOM生成,样式计算和布局三个阶段,那趁着今天有空继续后面的浏览器渲染阶段吧。
这里首先回忆下,在浏览器的渲染的前三个阶段是怎样的:在HTML页面内容被提交到渲染引擎之后,渲染引擎首先将HTML解析为浏览器可以理解的DOM元素;然后根据CSS样式表,计算出DOM树所有节点的样式;接着计算每个元素的几何坐标位置,并将这些信息保存在布局树中。
分层
我们现在有了布局树,并且每个元素的具体位置信息也都计算出来了,那么接下来是不是就要开始着手绘制页面了?当然不可以。
因为页面中有很多复杂的效果,如一些复杂的3D变换,页面滚动,或者使用z-indexing做z轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(layerTree)。若你熟悉PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起搞成了最终的页面图像。
想要更直观地理解什么是图层,你可以打开chrome的“开发者工具”,选择layer标签,就可以可视化页面的分层情况。
现在你知道了浏览器的页面实际上被分为了很多图层,这些图层叠加后合成了最中的页面。下面我们再来看看这些图层和布局树节点之间的关系,如下图所示:
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层,如上图的span标签没有专属的图层,那么他们就从属于它们的父节点图层。但不管怎样,最终每个节点都会直接或者间接地从属一个层。
那么需要什么条件,渲染引擎才会为特定的节点创建新的图层呢?通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
第一点,拥有层叠上下文属性的元素会被提升为单独的一层
页面是个二维平面,但是层叠上下文能够让HTML元素具有三维概念,这些HTML元素按照自身属性的优先级,分布在垂直于这个二维平面的z轴。通过下图可以直观感受到;
从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等,都拥有层叠上下文属性。
第二点,需要裁剪(clip)的地方也会创建为图层
不过首先需要知道什么是裁剪,结合下面的HTML代码:
所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:
从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。
图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。
这里我们把div的大小限定为200*200px,而div里面的文字内容比较多,文字所显示的区域肯定会超过200*200的面积,这时候就产生了裁剪,渲染引擎会把裁剪文字内容的一部分用于显示在div区域,出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。所以说,元素又了层叠上下文的属性或者需要被裁减的时候,满足其中任意一点,就会被提升为单独一层。
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层开始进行规制,那么渲染引擎是入会是图层绘制的?
渲染引擎在实现图层的绘制的时候,会把一个图层的绘制分成很多小的绘制指令,然后这些指令按照顺讯组成一个待绘制列表,如下图所示。
从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景,前景,边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。可以结合下图来看下渲染主线程和合成线程之间的关系:
上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)结合成线程,那么接下来合成线程是怎么工作的呢?
我们首先来看看什么是视口:屏幕上页面的可见区域就叫做视口(ViewPort)
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动很久滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制所有图层内容的话,就会产生太大的开销,而且也没必要。基于这个原因,合成线程会把图层划分为图块(tile),这些图块的大小通常是256*256或者512*512。
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内进行的,运行方式如下图所示:
通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫做快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。
GPU操作是运行在GPU进程中,如果栅格化操作使用了GPU,那么最终生成位图的操作就是在GPU中完成的,这就涉及到了跨进程操作。具体形式参考下图:
从图中可以看出,渲染进程把生成图块的指令发送给GPU,然后在GPU中执行生成图块的位图,并保存在GPU的内存中。
合成和显示
一旦所有的图块都被光栅化,合成线程就会生成一个绘制图块的指令 --- “DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz的组件,用来接受合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的HTML,CSS,JavaScript等文件,经过浏览器就会显示出漂亮的页面了。
渲染流水线大总结
我们现在已经分析出整个渲染流程,从HTML到DOM,样式计算,布局,图层,绘制,光栅化,合成和显示。如下图:
结合上图,一个完整的渲染路程大致可以总结为如下:
1 渲染进行将HTML内容转化为能够读懂的DOM树结构。
2 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。
3 创建布局树,并计算元素的布局信息。
4 对布局树进行分层,并生成分层树。
5 为每个图层生成绘制列表,并将提交到合成线程。
6 合成线程将图层分为图块,并在光栅化线程池中将图块转换成位图。
7 合成线程发送绘制图块命令DrawQuad给浏览器进程。
8 浏览器进程根据DrawQuad信息生成页面,并显示在显示器上。
关于浏览器的几个绘制概念
理解重排,重绘和合成。对后续的web性能优化有很大的帮助。
1,更新了元素的几何属性(重排)
从上图可以看出,如果你通过JavaScript或者CSS修改元素的几何位置属性,例如改变元素的宽度,高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫做重排。无疑,重排需要更新完成的渲染流水线,所以浏览器开销最大。
2,更新元素的绘制属性(重绘)
接下来,我们看看重绘,比如通过JavaScript更改某些元素的背景原色,渲染流水线会怎么调整呢?参照下图:
从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个极端叫做重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些
3,直接合成阶段
那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。
在上图中,我么使用了CSS的transform来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提成绘制效率。
总结
你可以发现,chrome的渲染流水线还是相当复杂晦涩,难以理解的,不过Chrome团队在不断添加新功能的同时,也在不断地重构一些子阶段,目的就是让整体渲染架构变得更加简单和高效。由此可见把复杂问题变得简单化的都是需要大智慧的。那么web性能优化中,减少重绘和重排是一种很好的优化方式。那又有哪些具体的实践方法来减少重绘,重排呢?尽量使用class操作样式,而不是频繁操作style,避免使用table布局,批量dom操作,列入createDocumentFragment 或者使用框架 react,Debounce window resize事件,对dom属性的读写要分离,will-change:transform做优化。