深入浏览器渲染原理

340 阅读9分钟

序言

最近看了些咱们前端项目运行在浏览器上的一些原理性的东西,之前写了一篇文章其中从浏览器的角度分析了js中的事件循环机制([从浏览器运行机制切入,理解事件循环机制](从浏览器运行机制切入,理解事件循环机制 - 掘金 (juejin.cn)))。今天这篇文章就主要说下,咱们的浏览器是如何把一个引入了样式,行为的html文件进行解析最终把一个个带有颜色的像素点画在咱们浏览器上的。

step.1 对HTML的解析

不知道你有没有想过,我们在浏览器上输入一个网址的时候,为什么一个精美的页面会出现在我们的浏览器页面中呢? 其实是因为我们对这个网址发起请求的时候,其对应的服务器给我们返回了一个html文件(当然其中也引入了外部样式,外部行为)。我们在开发中用到的那些框架,最后打包出来也还是html,css,js这样一个文件结构,只是框架在js文件中做了很多复杂的事情罢了。

例如,我们访问掘金的地址时,浏览器就接收到了这个:

1681302627085.png

浏览器接收到对应的html文件之后,会生成一个渲染任务,并添加到等待队列中,经过事件循环之后(事件循环其实反映的是渲染主线程与等待队列之间的关系,等待队列中除了放置js的相关异步任务以外,还有其他任务,比如这里说的渲染任务。有兴趣的话可以看看我的这篇文章去了解一下事件循环(从浏览器运行机制切入,理解事件循环机制 - 掘金 (juejin.cn))),这个渲染任务被添加到渲染主线程中进行执行。整个过程如下图所示:

WOOZFK@OA(D62MZEQVNM7T0.png

浏览器在解析html时,会一行一行从上到下依次对标签进行解析。遇到描述js的标签(如,script标签)浏览器会直接执行。遇到描述样式的标签(如,link标签,style标签)浏览器会对其中的样式进行下载和解析并最终生成 css规则树 也就是 cssom树。而解析除刚刚说到的这几类标签外的其余标签(我暂且称呼他们为纯html标签,因为这类标签的功能就是标注,代表他们就是页面中的一个个盒元素),则会生成 dom树 。这两个树其实就是为了方便渲染的后续操作,从而解析了文本生成了对象树(毕竟操作字符串可没有直接操作对象来的爽)。在很多地方都用到了这样的思想,比如vue框架中的模板解析也是通过解析template中的字符串生成ast语法树,然后根据ast语法树生成对应的render函数,从而生成虚拟dom树进而对真实dom树进行更新。

稍微说下,ast语法树是个什么东西(因为我自己之前也一直不明白)。其实,就是根据代码生成的描述代码的对象树。比如:

1681306154497.png

一段js代码,转换而成的ast语法树。

1681306248668.png

一段html代码,生成的ast语法树。

注意:在解析html的过程中,对纯html元素的解析和对描述js的标签的解析是在同一个线程上执行的,即,渲染主线程上。但是对于css样式文件的下载与解析,浏览器则会另开一个线程去处理。所以在解析的过程中,js代码的执行会影响html元素的解析,但不会影响样式的解析

step.2 样式计算

在完成上一步的操作之后,我们得到了 dom树 和 cssom树。接下来,浏览器根据生成的cssom树中的内容进行样式计算,将计算后的最终样式与dom树进行结合生成一个真正用于绘制页面的树,称呼其为渲染树(render树)。那么这里所说的样式计算是指的什么呢?我们看下图所示:

1681307393320.png

这是我们打开一个页面,选中一个元素所看到的他的样式信息。可以看到,字体大小还有宽高使用的相对单位rem。当打开 旁边的计算样式后,可以看到:

1681307576312.png

经过计算后,样式得到了确定的值,而不是之前的相对单位了。这就是样式计算所做的事情,还有我们平时做样式的时候会经常处理样式的权重问题,多个css选择器选中了同一个元素更改同一个样式,最后到底选哪个也是这样经过计算得来的。

step.3 布局 Layout

经过刚才的样式计算之后,我们得到了一个带有最终样式的dom树(render树)。但是,目前距离咱们完成把图案上的颜色一个个打在咱们的屏幕上的一个个像素点上还有很远的距离🤣,兄弟们坚持住。

那么这个layout究竟又做了些什么呢? 其实,在这一步浏览器会根据这个render树再生成一个布局树(layout树)。那这个时候就有人问了,这个render树和这个布局树有啥不一样捏?

这两颗树的区别还是挺大的。这个布局树是根据真实的几何信息而生成的树,用一张图来描述下什么叫做根据真实几何信息生成的树吧。

1681308760432.png

是不是一下子一目了然了呢。带有 display:none的盒子是没有宽高这些几何信息的表现在页面上就是什么也没有的效果。所以也就不会体现在布局树中了。而伪元素,虽然没有直接体现在dom树中,但是他是存在真实的几何信息的他是一个有宽高的真实的盒子。

1681309058339.png

step.4 分层 layer

接下来就是根据布局树在浏览器页面上正儿八经做绘制的工作了。但是,如果浏览器直接绘制上去的话,那么页面就只有一层的结构。当这个页面中的任何一个元素发生修改,比如,某个盒子高度变了,位置发生改变了,等这种会影响几何信息的变化。那么,整个页面就必须得进行重新排版,重新绘制了。浏览器为了提高效率,通过分层进行了优化。当页面中元素发生修改时,如果这种变化没有影响其他的图层,那重新布局,重新绘制就只会在对应的那个图层中进行更新了。 当我们打开chrome浏览器下的layer选项时,就能看到当前页面的图层信息了,如图所示。

1681481363010.png

不同浏览器对于分层的策略是不同的,有些css属性是可以影响分层结果的,比如 transform 属性。

step.5 绘制 paint

这里的绘制其实还不是指的把真实的图案,颜色绘制在咱们的屏幕上。这里的绘制指的是对每个图层生成对应的绘制指令集

指令集是什么?:

大家在使用 canvas 画图的时候,我们在代码中就需要对创建的画布对象下达指令。 比如,我们想要画一条直线,我们是不是就需要告诉画布对象这条直线的起点在那个位置,终点在那个位置,然后让他用 "画笔" 去把直线画出来。画一个矩形,我们需要告诉他这个矩形的参考点在哪,宽高各为多少,然后让他画。我们这里所说的生成的 指令集 作用就和这个是基本一样的。

到这步位置,渲染主线程的工作就已经结束了。我们刚才所说的所有的步骤都是由咱们的渲染主线程来完成的(除了解析html文件时,对样式进行下载,解析生成css规则树这一步,这是另外的线程做的)接下来的工作都是由合成线程来做的。

step.6 分块

分块是指对各个图层的区域进行更加精细的划分,让优先级高的块先被绘制指令集所使用,从而让其先被GPU绘制到浏览器中。这里 的优先级是由这个 是否接近咱们的视图窗口而决定的,越接近视口优先级越高。比如说,我们的页面中有一个高度达到2000px的盒子,但是咱们的显示器的高度只有700px,那么这个盒子顶部占据高度为700px的区域所在的那些 将会得到绘制指令的优先使用,最终优先渲染在咱们的屏幕上。

这张图描述了分块的概念: 左侧为图层,右侧为分块后的图层

1681484526897.png

这里需要注意一件事情,那就是分块这个工作是由合成线程创建出的多个子线程来做的(也就是说分块是多线程一起完成的)

step.7 光栅化

光栅化是指根据在上一步生成的每一个块来生成对应的位图的这一个过程。 位图其实就是我们常说的像素图,只是这里的位图上每一个 '像素点' 的位置存放的是颜色值。比如像 '#fff' 这样的颜色,效果如图所示: 左侧是每一个 ,右侧就是生成的位图。(这个过程是由 GPU 来做的)

)2VN3BUTK)G`Q3U70GM4Y6A.png

step.8 画

这就到了最后一步了,也就是根据位图上每个像素点的颜色值,然后把真正的颜色给画在屏幕上就行了。(这个过程当然也是 GPU 来做的了)