浏览器渲染原理
我们都知道浏览器有一个JS执行引擎,而执行页面解析的时候也有一个渲染引擎,但是渲染引擎在不同的浏览器中也是不同的,比如Firefox中渲染引擎叫做Gecko,在Chrome和Safari中都是基于Webkit开发的,这一章中我们主要来讲解Webkit的渲染过程。
接上一篇博客网页从输入网址到渲染完成经历了哪些过程,当我们接收到一个HTML文件后,浏览器会进行哪些操作将文件显示在页面上呢?
浏览器接收到HTML文件并转换成DOM树
首先,当浏览器接收到一个HTML文件,浏览器并不能马上就开始解析。我们平时写的代码都会分为HTML、CSS、JS文件,也就是字符串,但是计算机并不能识别这些字符串,所以在网络中传输的都是一些字节数据(0或1),将文件转换成字节数据这一操作主要都是在数据链路层中完成的,感兴趣的童靴可以去了解OSI七层模型。
当浏览器接收到这些字节数据后,它会将这些字节数据转换成字符串,也就是我们所写的代码。
当数据转换成字符串以后,浏览器会将这些这些字符创通过词法分析转换成标记(Token),这一过程在词法分析中叫做标记化
那什么是标记呢?这其实是编译原理这一块的内容了。简单来说,标记还是字符串,是构成代码的最小单位。这一过程会将代码拆成一块块的,并给这些内容打上标记,便于理解这些最小单位的代码是什么意思。
当结束标记后,这些标记会紧接着转换为Node,最后这些Node会根据之前不同的标记转换成一颗DOM树。
以上就是浏览器从网络上接收到HTML字节流后的一系列转换过程。过程总体可以概括为:
当然,在文件解析HTML文件的时候,浏览器还会遇到CSS和JS文件,这个时候浏览器也会去解析和下载这些文件。
将CSS文件转换为CSSOM树
其实转换CSS到CSSOM树的过程和HTML转换成DOM树的过程十分类似
在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程是很耗费资源的。因为样式你可以自己设置节点,也可以通过继承而获得。在这一过程中,浏览器需要递归CSSOM树,然后确定元素具体的样式是什么样的。
如果你有点不理解为什么会消耗资源的话,我这里举个例子
1 | <div> |
对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的span标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的span标签,然后找到span标签上的a标签,最后再去找到div标签,然后给符合这种条件span标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的CSS选择器,然后对于HTML来说也尽量少的添加无意义标签,保证层级扁平。
生成渲染树
当浏览器生成了DOM树和CSSOM树之后,就需要将这两棵树组合成render渲染树
在这一过程中,不是简单地将两者合并就可以了,渲染树只会包含需要显示的节点和这些节点的样式信息,如果某个节点是display:none的,那么就不会在渲染树中展示。
当浏览器生成render渲染树以后,就会根据渲染树的信息来进行页面布局(也可以叫做回流),然后调用GPU绘制,合成图层并显示在屏幕上,这一方面的知识我也不是太懂,感兴趣的童靴自行Google。
以上的三个步骤我们可以用一张图来间接说明:
是不是很好看,当然不是我画的,手动脸红。
通过上面的讲解我们可以初步的了解浏览器渲染页面的原理,下面我们来说说一些具体的情况。
为什么操作DOM慢
当我们使用JS操作DOM对象时,通常是通过JS线程去通知渲染线程,因为涉及到两个线程之间的通信,所以会造成一部分资源的浪费。此外,当我们改变了DOM对象,有很大的可能会造成浏览器的重绘和回流,浏览器需要重新渲染页面,所以也就导致了性能上的问题。在用户看来,这一部分操作就是慢。
重绘和回流
当render树中部分或者全部元素的尺寸、结构或者某些属性发生改变,浏览器重新渲染部分或者全部文档的过程叫做回流。
会导致回流的操作:
- 页面重新渲染
- 浏览器窗口大小发生改变
- 元素的尺寸或者元素的位置发生改变
- 元素的内容发生改变(文字数量或者大小等等)
- 元素字体大小的变化
- 添加或者删除可见的DOM元素
- 激活CSS伪类(例如:hover等属性)
- 查询某些属性或者调用某些API
当页面中元素样式的改变不会影响它在文档中的位置(例如: color、background-color、visibilify等),浏览器会将这些新样式赋予给元素并重新绘制它,这个过程就叫做重绘。
回流比重绘的代价更高,回流一定会发生重绘,但重绘不一定会发生回流。有时仅仅只是回流一个元素,它的父元素以及任何跟随它的元素都会产生回流操作。现代浏览器对频繁的重绘和回流操作进行了一个优化的操作:
浏览器会维护一个队列,把所有引起重绘和回流的操作放到一个队列中,如果队列中任务数量或者时间间隔达到了一个阈值,浏览器会将队列清空,然后进行一次批处理,把多次的重绘和回流转化成一次操作,怎么样,是不是感觉像是事件循环(Eventloop)类似?没错,重绘和回流其实也和Eventloop有关。
- 当Eventloop执行Microtasks后,会判断document是否需要更新,因为大部分浏览器是60HZ的刷新频率,也就是16.6ms执行一次。
- 然后判断是否有resize或者scoll事件,有的话会去触发事件,所以resize和scoll事件也至少16.6ms才会触发一次,并且自带了节流功能
- 判断是否触发了media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行requestAnimationFrame回调
- 执行IntersectionObserver回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
- 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行requestIdleCallback回调。
对于重绘和回流的Eventloop感兴趣的童靴可以去看看HTML文档。
如何减少重绘和回流
下面我们在来看看如何减少重绘和回流:
- 尽量避免style的使用,对于需要操作DOM元素节点,重新命名className,更改className名称。
- 如果增加元素或者clone元素,可以先把元素通过documentFragment放入内存中,等操作完毕后,再appendChild到DOM元素中。
- 不要经常获取同一个元素,可以第一次获取元素后,用变量保存下来,减少遍历时间。
- 使用 transform 替代 top
1 | <div class="test"></div> |
- 不要使用table布局,可能很小的一个小改动会造成整个table的重新布局
- 尽量少使用dispaly:none,可以使用visibility:hidden代替,dispaly:none会造成回流,visibility:hidden会造成重绘。
- 使用resize事件时,做防抖和节流处理。
- 对动画元素使用absolute / fixed属性。
- 批量修改元素时,可以先让元素脱离文档流,等修改完毕后,再放入文档流。
当然减少重绘和回流的方法肯定不止这一些,以上只是简单列举几种,感兴趣的童靴可以自己去查找资料。