前言
回流与重绘,听起来很高深的概念,其实它们两个只是重复执行浏览器渲染的过程而已。
所以,此博客将从浏览器渲染机制开始讲起
浏览器渲染页面过程
这里主要以webkit内核引擎的chrome浏览器进行讲解
先将图中的几个名词概念解释一下:
DOM Tree: 浏览器将HTML解析成树形的数据结构
Style Rules: 浏览器将CSS解析成树形的数据结构(也就是常说的CSSOM)
Attachment: 用来连接DOM与CSSOM以构建渲染树
Render Tree: Render Tree可以知道网页中包含的可见节点和样式信息
Layout: 根据生成的Render Tree,去进行计算得到节点的几何信息
Painting: 根据渲染树以及布局得到的几何信息,通过Painting得到节点的绝对像素
Display: 将像素发送给GPU,展示在页面上。
然后,可以总结浏览器的渲染过程
- 通过HTML Parser,解析HTML,生产DOM树,通过CSS Parser,解析CSS,生产CSSOM树
- 将DOM树和CSSOM树通过Attachment结合,生成渲染树(Render Tree)
- 根据生成的渲染树,进行Layout,得到节点的几何信息(位置,大小)
- 根据渲染树以及Layout得到的几何信息,通过Painting得到节点的绝对像素
- 在Painting后,渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU 形成渲染纹理。然后展示在页面上
渲染过程虽然只有5步,看似非常简单,但内部其实很复杂,让我们一步一步剖析
解析生成DOM树
从一个最简单的情况入手:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>解析DOM树</title>
</head>
<body>
<p>这是分析<span>解析生成DOM树</span> 的过程!</p>
<div><img src="1.jpg"></div>
</body>
</html>
浏览器是如何处理这个页面的呢?
- 转换: 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如UTF-8)将他们转换成各个字符
- 令牌化: 浏览器将字符串转换成规定的各种令牌,例如,,,以及其他尖括号内的字符串。每个令牌都具有特殊含义和一组规则。
- 词法分析: 发出的令牌转换成定义其属性和规则的"对象"。
- DOM构建: 最后,由于HTML标记定义不同标记之间的关系(一些标记包含在其他标记内),创建的对象链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系:HTML 对象是 body 对象的父项,body 是 paragraph 对象的父项,依此类推。
整个流程的最终输出是我们这个简单页面的文档对象模型 (DOM),浏览器对页面进行的所有进一步处理都会用到它。
浏览器每次处理 HTML 标记时,都会完成以上所有步骤:将字节转换成字符,确定令牌,将令牌转换成节点,然后构建 DOM 树。这整个流程可能需要一些时间才能完成,有大量 HTML 需要处理时更是如此。
CSSOM
引入了style.css,由于预见到需要利用该资源来渲染页面,它会立即发出对该资源的请求,并返回以下内容:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
而浏览器处理CSS的方式与处理HTML是一样的
再次为CSS执行上面几个步骤
最终也是会变成一个树结构的CSSOM
然后这也是CSS层叠样式表的原因,为页面上的任何对象计算最后一组样式时,浏览器都会先从适用于该节点的最通用规则开始(例如,如果该节点是 body 元素的子项,则应用所有 body 样式),然后通过应用更具体的规则(即规则“向下级联”)以递归方式优化计算的样式。
Render Tree
现在已经拥有了DOM和CSSOM了
- DOM:描述内容
- CSSOM: 描述需要对文档应用的样式规则
将通过Attachment将DOM树和CSSOM树合并形成渲染树(Render Tree)
此时渲染树上拥有网页上所有可见的DOM内容,以及每个节点的所对应的CSSOM样式信息。
为构建渲染树,浏览器大体上完成了下列工作
- 从DOM树的根节点开始遍历每个可见节点。
- 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
- 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,上例中的 span 节点—不会出现在渲染树中,—因为有一个显式规则在该节点上设置了“display: none”属性。
- 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
- 根据每个可见节点以及其对应的样式,组合生成渲染树
注意:渲染树只包含可见的节点
Layout
现在已经得到了渲染树
渲染树是由所有的可见节点以及节点对应的样式组合起来的,所以现在要将其在视口中显示出来
因此还需要计算它们在设备视口(viewport)内的几何信息(确切位置和大小),而这个计算的阶段就是Layout
Painting
根据渲染树以及布局得到的几何信息,通过Painting得到节点的绝对像素
Display
将得到的节点的绝对像素发送给GPU,展示在页面上
回流与重绘
在知道了浏览器渲染过程后,来看看所谓的回流重绘是什么并且其在何时发生?
回流是什么?何时发生回流?
回流:当浏览器发现某个部分发生了页面布局和几何信息的变化,就需要倒回去重新渲染了,重新渲染,就又要经过Layout计算可见节点在设备视口(viewport)内的几何信息,以及之后的Paiting和Display将这些信息渲染在页面上
这个重新计算可见节点的在设备视口(viewport)内的几何信息并渲染的过程就被称为回流
回流会从html这个根节点开始递归往下,依次计算所有可见节点的几何信息,且回流是无可避免的
总结一下:回流其实就是当浏览器中某个部分的页面布局或者几何信息发生变化时,就又会重新执行浏览器渲染的Layout和Paiting以及Display过程
只要行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都引起企它内部、周围甚至整个页面的重新渲染:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
重绘是什么?何时发生重绘?
重绘:改变某个元素的背景色、文字颜色、边框颜色等不影响它周围或内部布局的属性时,就会重新执行浏览器的Paiting和Display过程(元素的几何信息没有变)
只要是不改变元素的几何信息的都只会重绘:
- 改变元素的背景色
- 改变文字颜色
- 改变边框颜色
回流一定重绘,重绘不一定回流
在前面我们知道,回流是触发了浏览器渲染过程中的Layout,Paiting,Display,而重绘是触发了浏览器渲染过程中的Paiting,Display
也就是说回流的过程包括了重绘,如果触发了回流,那么必定重绘会随着浏览器渲染过程一起发生,所以回流一定重绘
而重绘的发生,并不会执行Layout,因此不一定会产生回流,所以重绘不一定回流
关于display:none和visibility:hidden
- display:none 的节点是不可见的,因此不会被加入Render Tree的,而visibility:hidden的节点会被加入Render Tree
- display:none 改为 display:block 时,算是增加了一个可见节点,因此会重新渲染,所以触发回流,而visibility:hidden,节点是已经在Render Tree中的,所以会触发重绘
浏览器的队列优化机制
因为每一次的回流与重绘都是非常消耗性能的,所以大多数浏览器引入了异步队列的形式,当改变元素样式时,并不会立刻回流或重绘一次,而是把这些操作加入到队列中,在经过至少一个浏览器刷新时间(16.6ms)或者队列中操作到达了一个阈值,才会清空队列,执行一次回流或重绘,这叫做异步重绘或增量异步重绘
但是,当执行一些获取布局信息的操作时,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。比如以下属性和方法:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- width、height
- getComputedStyle()
- getBoundingClientRect
所以,应该避免频繁的使用上述的属性和方法,因为它们都会强制渲染并刷新队列。
如何减少回流与重绘?
分别从CSS和JS两个层面去解答
从CSS层面减少回流与重绘
使用visibility代替display:none
因为visibility只会触发一次重绘,而display:none会触发回流(回流必重绘)
避免使用table表格布局
因为table的一个小改动都可能造成整个table的回流,而table及其内部元素的几何信息计算,是可能需要多次计算的,通常要比平常元素多花3倍时间!
一些复杂动画效果,使用绝对定位让其脱离文档流
一些动画效果,可能会频繁的改变几何信息(位置,尺寸,大小)和样式,所以会频繁的触发回流与重绘,因此可以使用绝对定位使其脱离文档流,这样就不会影响其他元素的布局,这样就减少了其影响其他元素布局从而产生的回流。
尽可能在DOM树的最末端改变class
回流是不可避免的,但可以通过在末端改变class,这样回流涉及到节点会变得很少。
CSS3硬件加速
CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。由于 GPU 中的 transform 等 CSS 属性不会触发 重绘,所以能大大提高网页的性能。
CSS 中的以下几个属性能触发硬件加速:
- transform
- opacity
- filter
- will-change
如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用一些小技巧来诱导浏览器开启硬件加速。
.element {
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
/**或者**/
transform: rotateZ(360deg);
transform: translate3d(0, 0, 0);
}
注意:GPU 渲染会影响字体的抗锯齿效果。这是因为 GPU 和 CPU 具有不同的渲染机制,即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。
创建独立图层
在GPU渲染过程中,一些元素会因为一些规则,被提升到独立的层,而独立出来后,是不会影响其他DOM的布局,就像脱离文档流一样,所以可以利用这一特性,将频繁变换的DOM元素主动提升到独立图层中,那么就可以减少Layout和Paiting的时间了
可以使用以下规则创建独立图层:
- 使用加速视频解码的 video 元素。
- 拥有 3D(WebGL) 上下文或者加速 2D 上下文的 canvas 元素。
- 3D 或者透视变换(perspective,transform) 的 CSS 属性。
- 混合插件(Flash)。
- 对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素。
- 拥有加速 CSS 过滤器的元素。
- 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)。
- 元素有一个兄弟元素在复合图层渲染,并且该兄弟元素的 z-index 较小,那这个元素也会被应用到复合图层。
避免设置多层内联样式
CSSOM树是树的数据结构,因此查找样式时,是递归去查找的,而这个递归过程是很复杂的,因此我们应该尽量少写过于具体的选择器和少添加无意义的标签
从JS层面减少回流和重绘
尽量减少多次修改DOM样式
最好一次性修改DOM的样式
比如下面的代码
const el = document.querySelector('#el')
el.style.backgroundColor = 'skyblue'
el.style.opicity = 0.5
el.style.width = 300 + 'px'
在之前提到过,浏览器是会将这些重绘与回流操作加入到队列中的,所以可能其最后只会执行一次,但是这里我改变了width,也就是会强制渲染并清空队列的属性,这时候就会触发三次回流与重绘,因此尽可能的将多次修改DOM样式合并成一次
可以使用cssText将其合并:
const el = document.querySelector('#el')
el.style.cssText += 'background-color: skyblue; opicity: 0.5; width: 300px;';
也可以通过添加class来实现:
.test {
background-color: skyblue;
opicity: 0.5;
width: 300px;
}
const el = document.querySelector('#el')
el.className += ' test'
避免频繁操作DOM
当我们操作DOM时,可以使用以下方法来减少回流与重绘
- 使其脱离文档流
- 进行DOM操作
- 将其添加回文档流中
脱离文档流和添加回文档这两次回流是无可避免的,但是中间的DOM操作,则是在Render Tree之外进行的,因此不会产生任何的回流与重绘
而脱离文档流,可以用两种方法
- display:none 使其节点不可见,因此能脱离渲染树
- documentFragment 文档片段
操作都很简单,就是display:none后或创建文档片段后对其进行DOM操作,之后再将其display:block和添加到文档中
display的伪代码:
div.style.display = 'none' // 节点不可见,触发一次回流
div.style.width = 200 + 'px' // 此时为不可见节点,脱离了Render Tree,因此不会产生回流和重绘
div.style.height = 300 + 'px'
div.style.display = 'block' // 节点可见,触发一次回流
documentFragment的伪代码:
const fragment = document.createDocumentFragment() // 触发一次回流
// 对fargment 进行DOM操作
div.appendChild(fragment) // 将文档片段插进DOM中,触发一次回流
避免频繁读取会引发回流/重绘的属性以及避免触发同步布局事件
访问一些引发回流或重绘甚至强制刷新队列的属性不能太频繁
如下,这段代码每次循环都会访问offsetWidth属性,因此会导致浏览器强制清空队列,进行强制同步布局
const box = document.querySelector('#box')
const box2Arr = document.querySelectorAll('.box2')
function boxWidth() {
for (let i = 0; i < box2Arr.length; i++) {
box2Arr[i].style.width = box.offsetWidth + 'px';
}
}
而如果我们使用一个变量将其存起来,这时只有第一次赋值变量时,才会清空队列,而后面循环读取的都是变量的值,不会触发清空队列
const box = document.querySelector('#box')
const box2Arr = document.querySelectorAll('.box2')
const width = box.offsetWidth
function boxWidth() {
for (let i = 0; i < box2Arr.length; i++) {
box2Arr[i].style.width = width + 'px';
}
}
总结
在了解了浏览器渲染机制后,再去看回流与重绘,是不是感觉挺简单的呢?
感觉有收获的朋友们!求点赞,关注,评论! 求三连!