浏览器是如何渲染页面的?

193 阅读7分钟

浏览器是如何渲染页面的?

当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开始渲染流程。

整个流程分为多个阶段:HTML解析、样式计算、布局、分层、绘制、分块、光栅化、画。每个阶段都有明确的输入输出,上个阶段的输出会作为下个阶段的输入。(流水线)

第一步:HTML解析 - Parse

解析过程中遇到css 解析css、遇到js 执行js。为了提高效率,浏览器在开始解析之前,会启动一个预解析的线程,率先下载HTML中的外部CSS文件和外部的JS文件。

如果主线程解析到link位置,此时外部的CSS文件还没有下载解析好,主线程不会等待,继续解析后续的HTML。这是因为 下载和解析CSS 的工作是在预解析线程中进行的。这就是CSS不会阻塞HTML解析的根本原因。

如果主线程解析到script位置,会停止解析HTML,等待JS文件下载好,并将全局代码解析执行完成后,才能继续解析HTML。这是因为js代码的执行过程可能会修改当前的DOM树,所以DOM树的生成必须暂停。这就是js会阻塞HTML解析的根本原因。

第一步完成后,会得到DOM树和CSSOM树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在CSSOM树中。

第二步:样式计算

主线程会遍历得到的DOM树,依次为树中的每个节点计算出它最终的样式,称之为Computed Style

在这一过程中,很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px。(百分比的宽高在这一步算不出来)

这一步完成后,会得到一棵带有样式的DOM树

第三步:布局 - Layout

依次遍历DOM树的每一个节点,计算每个节点的几何信息。布局完成后会得到布局树

大部分时候,DOM树和布局树并非一一对应:比如display:none的节点是没有几何信息的,因此它不会生成到布局树。又比如使用了伪元素选择器,虽然DOM树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致DOM树和布局树无法一一对应(会在下方解释)。

Layout布局的部分规则:

  1. 内容必须在行盒中(切记,说的不是 行级元素、块级元素,这些指的是html中的内容,而行盒、块盒 讲的是css中的内容)。
  2. 行盒和块盒不能相邻

在这一步骤中,例如遇到了<p>123</p>,在进行布局时,就会识别到 <p> 为块盒、123为内容,由于上述规则第一点,布局时会在块盒中加入一个行盒(用来放内容),而它被称为匿名行盒。相同的,如果在html中有一个裸露的内容,它同样会被匿名行盒包裹,但由于上述第二点,行盒与块盒不能相邻,因此在这个匿名行盒外部还会再包裹一个匿名块盒

第四步:分层 - Layer

主线程会使用一套复杂的策略对整个布局树进行分层。(以前的老浏览器是没有这一步的,现代浏览器都有了。)

好处: 将来某一层改变后,仅会对该层进行后续处理,从而提升效率。

如下图的有部分所示: image.png

我们无法主动决定分层结果:像滚动条、堆叠上下文、transform、opacity 等样式都会间接影响分层结果,也可以通过在css中will-change属性(will-change: transform;)告诉浏览器有哪个属性可能会更新,来更大程度影响分层结果。(但will-change不要滥用,一般不会一开始就使用,一定是渲染、效率出问题,调试发现为某一块经常需要变动等 才会去使用它)

第五步:绘制 - Paint

这里的绘制,并不是开始画像素的意思,而是为每一层单独生成绘制的指令集。(像canvas的一样,把画笔移动到某个点、怎么画、怎么填充颜色等等)

渲染主线程就做到这一步,后续步骤交给其他线程完成。(这是浏览器做出的极大优化

渲染主线程->parse->style->Layout->Layer->Paint

其他线程------------------------------------------------>后续步骤

第六步:分块 - Tiling

  1. 完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。 (分块会把每一层分为多个小的区域,分块的工作是交给多个线程同时进行的。)
  2. 合成线程首先对每个图层进行分块,将其划分为更多的小区域。
  3. 之后它会从线程池中拿取多个线程来完成分块任务,最后再一起收集到合成线程中。

第七步:光栅化 - Raster

光栅化是将每个块变成位图(每个像素点的信息)优先处理靠近视口的块。光栅化这个步骤,合成线程会交给 浏览器的GPU进程(进程哝) 用到GPU加速,生成一块一块的位图交给合成线程。

第八步:画 - Draw

  • 合成线程拿到每个层、每个块的位图后,生成一个个的【指引(quad)】信息。
  • 指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。(注意这里是相对屏幕而不是相对页面,和绘制不同)
  • ⭐变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。
  • 合成线程会把quad提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像。

完整过程

渲染主线程->parse->style->Layout->Layer->Paint↘

合成线程--------------------------------------------->titling->raster->draw->GPU

什么是reflow?

flow:排版; reflow:重新排版;效率影响是比较大的。

  • reflow的本质就是重新计算Layout树
  • 当进行了会影响布局树的操作后,需要重新计算布局树,会引发reflow。
  • 为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当js代码全部完成后再进行统一计算。所以,改动属性造成的reflow是异步完成的。
  • 但在浏览器的反复权衡下,最终决定获取属性的操作立即reflow
dom.style.width = xxx
dom.style.height = xxx
dom.style.margin = xxx
// 以上就是三次操作了
// 这时候修改了cssom树之后要重新计算layout(布局)树,之后再把渲染任务放到消息队列中
// 意味着此时页面还没有刷新,如果我们此时去拿取这个dom的宽度,依然会是旧的值
dom.clientWidth
// 所以浏览器最后改为:获取属性的操作会立即reflow,  这样就能读到最新的值

所以有时候,我们为了强行让它重新布局,就去读它的属性。因为读取就可以立即reflow。

什么是repaint?

  • paint在上面学习中的第五步,repaint的本质就是重新根据分层信息计算了绘制指令。
  • 改动了可见样式后,就需要重新计算,会引发repaint。
  • 由于元素的布局信息也属于可见样式,所以reflow一定会引发repaint(按顺序执行嘛)。

为什么 transform 效率高?

因为如果使用js去改变transform,只会影响CSSOM树(也就是样式计算那一步),其他步骤都不需要重新执行,直到最后的draw步骤才重新绘画。而如果是使用animation+transform去做动画的话,甚至只有draw需要重新执行。