浏览器是如何渲染页面的?
当浏览器的网络线程收到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布局的部分规则:
- 内容必须在行盒中(切记,说的不是 行级元素、块级元素,这些指的是html中的内容,而行盒、块盒 讲的是css中的内容)。
- 行盒和块盒不能相邻。
在这一步骤中,例如遇到了<p>123</p>,在进行布局时,就会识别到 <p> 为块盒、123为内容,由于上述规则第一点,布局时会在块盒中加入一个行盒(用来放内容),而它被称为匿名行盒。相同的,如果在html中有一个裸露的内容,它同样会被匿名行盒包裹,但由于上述第二点,行盒与块盒不能相邻,因此在这个匿名行盒外部还会再包裹一个匿名块盒。
第四步:分层 - Layer
主线程会使用一套复杂的策略对整个布局树进行分层。(以前的老浏览器是没有这一步的,现代浏览器都有了。)
好处: 将来某一层改变后,仅会对该层进行后续处理,从而提升效率。
如下图的有部分所示:
我们无法主动决定分层结果:像滚动条、堆叠上下文、transform、opacity 等样式都会间接影响分层结果,也可以通过在css中will-change属性(will-change: transform;)告诉浏览器有哪个属性可能会更新,来更大程度影响分层结果。(但will-change不要滥用,一般不会一开始就使用,一定是渲染、效率出问题,调试发现为某一块经常需要变动等 才会去使用它)
第五步:绘制 - Paint
这里的绘制,并不是开始画像素的意思,而是为每一层单独生成绘制的指令集。(像canvas的一样,把画笔移动到某个点、怎么画、怎么填充颜色等等)
渲染主线程就做到这一步,后续步骤交给其他线程完成。(这是浏览器做出的极大优化)
渲染主线程->parse->style->Layout->Layer->Paint
其他线程------------------------------------------------>后续步骤
第六步:分块 - Tiling
- 完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。 (分块会把每一层分为多个小的区域,分块的工作是交给多个线程同时进行的。)
- 合成线程首先对每个图层进行分块,将其划分为更多的小区域。
- 之后它会从线程池中拿取多个线程来完成分块任务,最后再一起收集到合成线程中。
第七步:光栅化 - 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需要重新执行。