前言
我们都知道,我们编写好的HTML,CSS,JavaScript文件,在经过浏览器处理后就可以变成漂亮的页面,但是估计很多人都不知道,浏览器是如何将它们转化成页面的,总结来说,这之间共经历了八个步骤,分别是构建DOM数,样式计算,布局阶段,分层,绘制,分块,光栅化,合成,至于每个步骤都做了什么,我们会在文中详细解释。
构建DOM树
为什么要构建DOM树?这是因为;浏览器无法直接理解和使用 HTML,所以需要将HTML转化成浏览器能够理解的结构--DOM树。至于DOM树是如何构建的,我们可以看下下面这这张示意图:
从图中可以看出,我们的HTML文件,经过HTML解析器解析之后,最终输出树状的DOM。
为了更加直观的理解DOM树,你可以打开浏览器的开发者工具,在控制台输入document,然后 回车,就可以看到一个完成的DOM数结构。
图中的document就是DOM结构,你可以看出,DOM和HTML内容几乎是一样的,不同的是,DOM是保存在内存中的树结构,可以通过JavaScript来查询或者修改其内容。
现在,我们就已经生成了DOM树了,但是我们还不知道节点的样式,要让节点拥有正确的样式,接下来就要进行样式计算了。
样式计算(Recalculate Style)
样式计算的目的就是为了计算出DOM节点每个元素的样式,这个阶段大概可以分成三个步骤来完成。
1. 把CSS转换成浏览器能够理解的结构
首先,我们先了解下CSS的来源,如下所示:
从图中可以看出,CSS样式来源主要有如下三种:
- 通过link引入外部CSS文件
- 标签内的CSS
- 元素的style属性内嵌的CSS 和HTML一样,浏览器也无法直接理解和使用这些纯文本的CSS样式,所以当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换成浏览器可以理解的结构--styleSheets。
为了加深理解,我们可以在Chrome浏览器的控制台中查看其结构,只需要在控制台中输入document.styleSheets,然后回车就能看到如下图所示的结构:
从图中可以看出,这个样式包含了很多种样式,已经把三种来源的样式都包含进去了,同时我们也可以查询和操作这些样式。
2. 转换样式表中的属性值,使其标准化
现在,我们已经把CSS文本转换成了浏览器可以理解的结构了,接下来就要对其进行属性的标准化操作。
那什么是属性值标准化呢?我们先看如下CSS文本:
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
可以看出,上卖弄的CSS文本中有很多属性值,如2em,blue,bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换成渲染引擎容易理解的、标准化的值,这个过程就是属性值标准化。
下图就是属性值标准化之后的样子了:
从图中可以看出,2em被解析成了32px,bule被解析成了rgb(0,0,255),bold被解析成了700等
3.计算DOM数中每个节点的具体样式
现在属性值也已经标准化了,接下来就应该计算每个节点的样式属性了,如何计算呢? 这就涉及到CSS的继承规则和层叠规则了。
首先是CSS继承,CSS继承就是每个DOM节点都包含父节点的样式,比如如下例子:
body { font-size: 20px }
p {color:blue;}
span {display: none}
div {font-weight: bold;color:red}
div p {color:green;}
这张样式表最终与应用到DOM节点的效果如下图所示:
从图中可以看出,所有子节点都继承了父节点样式,比如body节点的font-size属性是20,那body节点下面的所有节点的font-size都等于20.
样式计算的第二个规则就是样式层叠,层叠是CSS的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法,他在CSS处于核心地位,CSS的全称‘层叠样式表’就是强调了这一点。具体规则这里就不介绍了,相比大家应该都知道,就算不知道,网上资源也很多,大家自行百度就行了。
总之,样式计算阶段的目的就是计算出每个DOM的样式,在计算过程中需要遵守CSS的继承和CSS的层叠两个规则,每个阶段最终输出的内容是每个DOM节点的样式,并保存在ComputedStyle的结构内。
布局阶段
现在,我们已经有了DOM树和每个DOM节点的样式了,但这不足以显示页面,因为我们还不知道DOM元素的几何位置的具体信息,你们接下来就需要计算出DOM树中可见元素的几何位置,我们把这个计算过程叫做布局。 这个阶段需要完成两个任务,创建布局树和布局计算。
1.创建布局树
DOM树中含有很多不可见的元素,比如head标签以及使用了display:none的元素,所以在显示之前,我们还要额外地构建一颗只包含了可见元素的布局树。
我们可以结合下图来看看布局树的构造过程:
布局树构造示意图
从上图可以看出,DOM数中所有不可见的节点都没有包含在布局树中,为了完成布局树,浏览器大体上完成了下面这些工作:
- 遍历DOM树中所有可见节点,并把这些节点加到布局树中;
- 不可见的节点会被布局树忽略掉,如head标签以及包含可diaplay:none属性的节点,如body.p.span元素。
2.布局计算
现在我们已经有了完整的布局树了,接下来就要计算节点的坐标位置了,但是这个计算过程非常复杂,这里就先不讲,我们只需要知道他是计算节点的坐标位置的就行了。
分层
虽然我们已经有了布局树,并且每个元素的具体位置也有了,但是我们依然不能着手绘制页面,因为页面中还有很多复杂的效果,如一些复杂的3D变换,页面滚动,或者使用z-index做了z轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。 浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面,下面我们再来看看这些图层和布局树节点之间的关系,如下图所示:
通常情况下,并不是布局树中的每个节点都包含一个图层,如果一个节点没有对应的涂层,那这个节点就从属于父节点涂层,如上图中的span标签没有专属涂层,那么它们就从属于它们的父节点涂层,但不管怎样,最终每个节点都会直接或间接地从属于一个图层。
那么需要满足什么条件,渲染引擎才会为特定的节点创建新的图层呢?通常满足下面两点中任意一旦就可以被提升为一个单独的图层。
1.拥有层叠上下文属性的元素会被提升未单独的一层
页面是二维平面,但是层叠上下文能够让HTML元素具有三维概念,这些HTML元素按照自身属性的优先级分布在垂直于这个二维平面的z轴上,可以结合如下图来直观感受下:
层叠上下文示意图
从图中可以看出,明确定位属性的元素,定义透明属性的元素,使用CSS滤镜的元素等,都拥有层叠上下文属性。2.需要剪裁(clip)的地方也会被创建为图层
什么是剪裁呢?当我们的div大小限定,但是div里面的内容比较多,文字所显示的区域肯定会超出div的面积,这个时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在div区域。
出现裁剪情况的时候,渲染引擎会为位子单独创建一个层,如果出现滚动条,滚动条也会被提升为一个单独的层。
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。
渲染引擎的图层绘制与我们平时绘画类似,它会把一个图层拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制的列表。
格栅化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的,你可以结合下图来看下渲染主线程和合成线程之间的关系:
如上图所示,当图层的绘制列表准备好之后,主线程会把改绘制列表提交给合成线程,那合成线程是如何工作的呢?
通常一个页面可能很大,比如有的页面你使用滚动条要滚动好久才能滚到底部,但是通过视口,用户只能看到页面中很小的一部分,所以在这种情况下,要绘制处所有的图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为土块,这些土块的大小通常是256×256或者512×512。 然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由格栅化来执行的,所谓格栅化,是指将图块转化成位图。。而图块是格栅化执行的最小单位,渲染进程维护了一个光栅化的线程池,所有的图块格栅化都是在线程池内执行的,运行方式如下图所示:
合成线程提交图块个格栅化线程池
通常,格栅化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速格栅化,或者GPU格栅化,生成的位图被保存在GOU内存中。合成和显示
一旦所有的图块都被格栅化,合成线程就会生成一个绘制图块的命令--'DrawQuad',然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内容显示在屏幕上。
到这里,经过一系列的阶段,编写好的HTML,CSS,JavaScript等文件,经过浏览器就显示出漂亮的页面啦!
总结
我们来对上面的流程进行总结,如下:
- 渲染进程将HTML文件转换成DOM树结构
- 渲染引擎将CSS样式表转换成浏览器可以理解的StyleSheets,并计算出DOM节点的样式;
- 创建布局树,并计算出元素的位置信息;
- 对布局树进行分层,并生成分层树;
- 为每个图层生成绘制列表,并将其提交到合成线程;
- 合成线程将图层分成图块,并在光栅化线程池将图块转换成位图;
- 合成线程发送绘制图块命令DrawQuad给浏览器进程;
- 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上;
站在渲染流程的角度上理解重绘,重排和合成;
-
更新了元素的几何属性(重排)
参考下图:
更新元素的几何属性
从上图可以看出,如果你通过JavaScript或者CSS修改了元素的集合位置信息,例如改变元素的宽高等,你们浏览器就会触发重新布局,解析之后的一系列子阶段,这个过程就叫**重排**,很明显,重排需要更新完整的渲染流水线,所以开销也是最大的。-
更新了元素的绘制属性(重绘)
接下来,我们看看重绘,比如通过JavaScript修改了元素的背景颜色,渲染流水线会怎样的调整呢?
更新元素的背景颜色
从图中可以看出,如果修改了元素的背景颜色,你们布局阶段将不会被执行,因为没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫做**重绘**,相比于重排操作,**重绘省去了布局和分层阶段,所以执行效率比重排要高一些。**- 直接合成阶段
那如果你更改了一个既不要布局也不要绘制的操作,会发生什么变化呢?渲染引擎将跳过布局和绘制,值执行后面的合成操作,我们把这个过程叫做合成。具体流程如下:
避开重绘和重排
从上图中,我们使用了CSS的transform来实现动画效果,这就可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作,这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以**相比于重绘和重排,合成能大大提升绘制效率。**理解了渲染流程以及重绘重排等概念,我们也就知道了为什么减少重绘和重排能优化web性能了.