浏览器渲染流程笔记

40 阅读11分钟

首先上个大纲

  • 构建DOM树
  • 构建CSSOM树(样式计算),计算出DOM节点的样式
  • 构建布局树(布局阶段),并计算元素的布局信息
  • 分层,对布局树进行分层,生成分层树
  • 绘制,为每个图层生成绘制列表,并将其提交到合成线程
  • 分块,合成线程将图层分成图块
  • 光栅化,在光栅化线程池中将图块转化为位图。
  • 合成,合成线程发送绘制图块命令DrawQuad给浏览器线程。
  • 浏览器根据DrawQuad消息生成页面,并显示到显示器上。

下面我们要从每个阶段的输入,处理,输出的过程来讲解。

构建DOM树

为什么要构建DOM树?

因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能理解的结构:DOM树。

来看看如何构建DOM树

Image.png

具体如何构建DOM呢?

浏览器会遵循一套步骤将HTML文件转换为DOM树,宏观上分为这几个步骤

网络获取数据->获取字节数据(0 1)->转化为字符串('<...>')->字符串转Token( )->node->DOM

前面的都不算很关键,我认为关键的是Token转node转DOM。

将字符串转换为Token例如等,Token中会标识出当前Token是开始标签还是结束标签还是文本内容,这样的标识的作用就能让我们知道节点与节点的关系。

Image2.png

然后就是生成节点对象并构建DOM。

注意:在构建DOM过程中,是一边生成Token一边转换Tokne来生成节点。

样式计算(构建CSSOM树)

样式计算的目的就是为了计算出DOM节点中每个元素的具体样式,这个阶段有两步。

  • 将css转化为cssom树(对象)
  • 标准化样式表属性值

首先是将css转化为浏览器能理解的结构。

那我们要清楚css样式的来源,不同于html文件直接了当,css样式的来源有三种:

Image3.png

  • 通过link引用外部样式
  • 标记内的css
  • 元素内嵌的css样式

和html文件一样,浏览器也无法理解这种纯文本的css样式,所以当渲染引擎接收到css文本时,会执行一个转换操作。将css文本转化为浏览器可以理解的结构:styleSheets。

这个过程跟构建DOM树类似。

获取字节数据(0 1)->转化为字符串 ->字符串转Token ->node- >CSSOM

styleSheets的样子你可以通过document.styleSheets来查看

Image4.png

注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,且查找样式表是按照从右到左的顺序匹配的例如:div p {...},会先寻找所有p标签并判断它的父标签是否为div之后才决定要不要采用这个样式渲染。所以平时写css尽量用css或者id,不要过度层叠

我们现在有了styleSheets,接下来就要把样式表中的属性值标准化。

转换样式表中的属性值,使其标准化

现在我们已经把CSS文本转变为浏览器可以理解的结构了。那么我们接下来就要对其属性值进行标准化操作。通俗来讲就是把那些不好理解的转化成浏览器容易理解的值。

Image5.png

2em解析成了32px,red解析成了rgb(255,0,0),bold被解析成了700......

到这里,样式属性标准化已完成。接下来就是计算每个节点的具体样式了。

这就涉及到css继承规则和层叠规则了

Image6.png

这里很简单,也就不多说了。

布局阶段

现在我们有了DOM树和CSSOM树。

接下来就要用DOM树和CSSOM树来构建布局树(render tree)了。我们还不知道DOM元素的几何信息。那么接下来计算出DOM树种可见元素的几何位置,这个过程就叫布局。

布局阶段需要完成两个任务:创建布局树和布局计算。

首先创建布局树,浏览器大体上进行了以下步骤:

  • 遍历DOM树中的所有可见节点,并把这些节点加到布局中。

  • 不可见的节点会被布局树忽略,如head标签下的内容,在比如display: none,他们都不会被包进布局树

Image7.png

现在我们有了一颗完整的布局树,接下来就要计算布局树节点的坐标位置了。

计算树节点的坐标位置。过程有点复杂,有兴趣自行百度。简而言之,就是计算布局树中各个可见元素在页面的坐标位置。

分层

有了完整的布局树不代表我们就能着手绘制页面了。

页面中有很多复杂的效果,比如说一些复杂的3D变换,页面滚动,使用z-index,如果就这样会在一张纸上,从布局树直接生成目标帧的话,那么每次页面又很小的变化,都会触发重排挥着重绘机制,这样的牵一发而动全身的绘制策略很影响页面的渲染效率。

而且页面并非单纯的平铺就可以了,实际上,页面还有许多复杂的3D变换、页面滚动等。为此,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。

Image8.png

所以chrome引入了分层和合成的机制。对于拥有层叠上下文属性的元素,渲染引擎会为其创建新的渲染图层。页面是个二维平面,但是层叠上下文能够让 HTML 元素具有三维概念。

我们可以在浏览器中通过浏览器开发者工具中Layers标签直观地看到图层的分层效果。

Image9.png

层的种类有两种,渲染图层(render layer)和复合图层(graphics layer)。两者的出现都是为了让HTML元素在2D平面堆出3D的视觉效果。这里想详细了解两个图层的话可以看复合图层,渲染图层及性能优化这里。

Image10.png

从图中看出明确定位属性的元素,定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文层叠上下文属性。目前,说的都只是渲染层层面的,用到的硬件加速是在哪里呢?那是对复合图层而言的,不在此展开。

实际上浏览器的页面被分为了很多图层,这些图层叠加后合成了最终的页面。这里结合ps的图层来理解更好。

图层绘制

对于图层,计算机还是看不懂,显示器也不懂怎么绘制。那渲染引擎就要对图层树的每个图层进行绘制,也就是生成绘制指令。

通俗来理解就是,教显示器画画。比如说你要画一个正方形,那你就要知道从哪里下笔,然后画多长的线,线要多粗,要什么颜色吧?先画什么,再画什么这里面就可以拆分为很多个绘制指令。每个元素就是一个绘制块,每个绘制块就有很多条绘制指令,多个绘制块就组成了一个绘制列表。

Image11.png

绘制列表就是用来记录绘制顺序和绘制指令的列表。打开Layers面板,双击左侧document节点,下面就会展开一个Profiler面板,里面有清晰的绘制流程,懂canvas的同学应该对这些指令会有点眼熟。

我直接上一个gif直观感受下

1231.gif

可以看出,这里的绘制流程就是一步一步画出来的。

分块

绘制列表只是用来记录绘制顺序和绘制指令的列表,实际上绘制操作是由渲染引擎中的合成线程来完成的。

当主线程吧绘制列表提交到合成线程,具体会发生什么?

在此之前我们需要先清楚一个概念:视口。视口就是屏幕上页面的可见区域。

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是256x256或者512x512,如下图所示:

Image12.png

栅格化

上面合成线程把图层划分为图块。

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,就是将几何信息转换为屏幕上的像素的过程。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:

Image13.png

通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。相信你还记得,GPU操作是运行在GPU进程中,如果栅格化操作使用了GPU,那么最终生成位图的操作是在GPU中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:

Image14.png

从图中可以看出,渲染进程把生成图块的指令发送给GPU,然后在GPU中执行生成图块的位图,并保存在GPU的内存中。

合成线程可以给不同的光栅线程赋予不同的优先级(prioritize),进而使那些在视口中的或者视口附近的页面可以先被光栅化。为了响应用户对页面的放大和缩小操作,页面的图层(layer)会为不同的清晰度配备不同的图块。

问:栅格化如果采用了GPU加速,能否认为整个浏览器都是采用硬件加速绘制的?

这只能说明在渲染流水线的某小阶段采用了GPU硬件加速,并非说浏览器开启了硬件加速那么浏览器页面就是交给GPU处理的。实际上还是需要经由渲染引擎一步步处理,到了光栅化这一步才交给GPU处理。而我们说的硬件加速,是在知道DOM树和样式之后便独立出一个复合图层,直接跳过了布局分层绘制阶段,只会触发绘制后的渲染流水线更新,一般作用于CSS 动画中(详细的可以看复合涂层和渲染图层)

合成和显示

所有的图块被光栅化转化为位图后,合成线程会生成一个绘制图块的命令DrawQuad,然后该指令提交给浏览器进程,浏览器接收到DrawQuad命令,从GPU内存中读取图片输出到显卡后缓冲区,显卡将后缓冲区内容交换至前缓冲区,由屏幕已60HZ的频率刷新显示图片

经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了

总结

Image15.png

大致渲染流程回顾:

  • 构建DOM树:把HTML转化为能读懂的DOM树结构
  • 构建CSS树:把CSS转化为能读懂的StyleSheets,计算出DOM节点的样式
  • 创建布局树:计算元素布局信息
  • 创建分层树:对元素分层,创建分层树
  • 生成绘制列表:为每个图层创建分层列表,并提交到合成线程
  • 分块:合成线程将图层分成图块,并在光栅化线程池中将图块转化为位图(视口附近的图块)。
  • 栅格化:将图块生成位图的操作称为栅格化,渲染进程维护了栅格化线程池,来完成图块到位图的转换,在栅格化过程中,还用到了GPU进程来加速位图的生成,使用GPU生成位图保存在GPU内存中,这个过程为快速栅格化的过程。
  • 发出Draw Quad指令:所有图块被光栅化转化为位图后,合成线程会生成一个绘制图块的命令DrawQuad发送给浏览器进程。
  • 输出显示:浏览器进程将图像发送给显卡的后缓冲区,后缓冲区和前缓冲区不断的交替使用,已到达显示器60HZ的速率刷出图像

参考文章:

blog.csdn.net/wangfeijiu/… 层叠上下文 渲染图层 复合图层(硬件加速)区别与联系 blog.poetries.top/browser-wor… 分层 cloud.tencent.com/developer/a…