计网基础-浏览器渲染+回流重绘+async/defer

loading 2023年01月01日 87次浏览

1.页面加载过程

在分析浏览器的渲染过程前,首先应对页面的加载过程有了解:

  • 浏览器根据DNS服务器得到域名对应的IP地址
  • 向IP地址的服务器发送HTTP请求(三次握手)
  • 服务器接受、处理并返回HTTP请求
  • 浏览器得到返回的内容后进行解析(一堆HTML格式的字符串)

2. 浏览器渲染过程

2.1 构建DOM树

浏览器会遵守一套步骤将HTML文件转换为DOM树。

  • 浏览器从磁盘或网络读取HTML的原始字节(0和1),并根据文件的编码(如UTF-8)将这些字节转换为字符串
  • 字符串转换成Token,例如:、等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息。
  • 一边生成Token一边消耗Token来生成节点对象,也就是说,每个Token被生成后就会被立即消耗掉来创建出节点对象。
  • 不同节点对象组合成了DOM树

2.2 构建CSSOM树

DOM树会捕获页面的内容,但是浏览器还需要知道页面如何展示,因此需要构建CSSOM树。

构建CSSOM的过程与构建DOM的过程非常相似,总体流程也是浏览器接收到CSS字节数据->字符串->token->节点->CSSOM。

在这一过程中,浏览器会确定每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为节点样式可以自行设置,也可以通过继承获得。在这一过程中,浏览器得递归CSSOM树,然后确定具体的元素到底是什么样式。

2.3 构建渲染树

成功生成DOM树和CSSOM树后,就要将这两棵树组合为渲染(Render)树。

  • 从DOM树的根开始构建,遍历每个可见节点
  • 对于每一个可见节点,寻找对应的CSSOM规则
  • 将可见节点和对应的CSSOM规则结合形成Render Tree

不可见节点:

  • Head标签下的所有子节点(meta\link)
  • script
  • display:none的元素

2.4 js阻塞

渲染过程中如果遇到js文件就停止渲染,转而执行JS代码,因为浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。

2.4.1 本身阻塞DOM

JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。

也就是说,如果想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将script标签放在body标签底部的原因。当然并不是说script标签必须放在底部,因为你可以给script标签添加defer或者async属性。

2.4.2 连带CSSOM阻塞DOM

JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建

原本DOM和CSSOM的构建互不影响,但是一旦引入了JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DOM再恢复DOM构建。

这是因为JavaScript不只是可以改DOM,它还可以更改样式,也就是它可以更改CSSOM。因为不完整的CSSOM是无法使用的,如果JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和 DOM构建,直至其完成CSSOM的下载和构建。 也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后再继续构建DOM。

3. 回流和重绘

3.1 概念

浏览器经过2.3的生成渲染树后,就会在渲染树上运行布局以计算每个节点的几何体,即布局阶段(回流和重绘)

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。

布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素,即最后的绘制阶段

那么什么是回流和重绘呢?

回流:
当我们对DOM的修改引发了DOM几何尺寸的变化(比如宽高,隐藏元素等),其他元素的几何属性和位置也会因此受到影响。浏览器需要重新计算元素的几何属性(在设备视口内确切的位置和大小),然后再将计算的结果绘制出来,这个过程称为回流。

重绘:
当我们对DOM修改了样式,却未影响其集合属性(比如只是修改了颜色),浏览器此时不需要重新计算元素的集合属性,可以直接为该元素绘制新的样式,这个过程称为重绘。

3.2 要点

当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流+重绘或者只有重绘。回流必定会发生重绘,重绘不一定会引发回流。

重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能,回流所需的成本比重绘高的多。

3.3 优化

那么该如何减少回流和重绘呢?

  • 合并多次对DOM和样式的修改,集中处理(通过cssText或者将样式都写在一个class里后直接修改class)
  • 使节点脱离文档流,然后进行会触发大量回流的操作,再令其回到文档流(比如通过display:none,position:absolute等等)
  • 需要用到offsetXXX,scrollXXX,clientXXX等属性时,把它们的值保存起来再使用
const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}
  • 使用 translate 替代 top/bottom/left/right
  • CSS3硬件加速

4. async和defer

蓝色线代表JavaScript加载;
红色线代表JavaScript执行;
绿色线代表HTML解析。

4.1 没有async/defer

<script src="script.js"></script>

没有defer或async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。

可以看到js加载和执行期间DOM树的构造被完全阻塞。

4.2 async 异步下载

<script async src="script.js"></script>

async属性表示异步执行引入的 JavaScript。

js加载过程和HTML解析过程并行,但是只要js加载好了,就会开始执行,无论此刻是HTML解析阶段还是DOMContentLoaded触发之后。

需要注意的是,这种方式加载的JavaScript依然会阻塞load事件。换句话说,async-script可能在DOMContentLoaded触发之前或之后执行,但一定在load触发之前执行。

两种情况

(1) HTML还没有被解析完的时候,async脚本已经加载完了,那么HTML停止解析,去执行脚本,脚本执行完毕后触发DOMContentLoaded事件。

(2) HTML解析完了之后,async脚本才加载完,然后再执行脚本,那么在HTML解析完毕、async脚本还没加载完的时候就触发DOMContentLoaded事件

4.3 defer 延迟执行

<script defer src="script.js"></script>

defer属性表示延迟执行引入的JavaScript。

js加载过程和HTML解析过程并行,但是只有等HTML和js都加载解析完后,才执行js代码。

整个document解析完毕且defer-script也加载完成之后(这两件事情的顺序无关),会执行所有由defer-script加载的JavaScript代码,然后触发DOMContentLoaded事件。

也是两种情况

(1) HTML还没解析完defer-script就已经加载完了,那么等HTML解析完后执行script,执行完毕后触发DOMContentLoaded事件

(2) HTML解析完了之后,defer脚本才加载完,然后再执行脚本,脚本执行完毕后触发DOMContentLoaded事件

在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。

4.4 DOMContentLoaded和load

DOMContentLoaded:初始的HTML文档被完全加载和解析后该事件就被触发,无需等样式表,图像,子框架等内容的加载。

load:当一个资源还有其所有依赖资源完成加载时,触发load事件