一、浏览器渲染过程
讲回流和重绘之前,我们先来了解下浏览器的渲染过程,如下图:
从图中看到,浏览器的渲染过程分以下几步进行:
1、将HTML解析为DOM树,将CSS解析为CSSOM树
2、将DOM树和CSSOM树进行计算合成,生成渲染树Render Tree
3、Layout(回流):根据渲染树Render Tree进行回流Layout,得到节点的几何信息(位置、大小)
4、Painting(重绘):根据渲染树Render Tree,以及回流(Layout)得到的节点几何信息,得到节点的绝对像素
5、Display:将各个节点绘制在屏幕上
其中,浏览器构建渲染树Render Tree过程,如下图:
1、从 DOM 树的根节点开始遍历每个可见节点。
- 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
- 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,上例中的 span 节点不会出现在渲染树中,因为有一个显式规则在该节点上设置了“display: none”属性。备注:利用visibility和opacity隐藏的节点,还是会显示在渲染树上的
2、对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们。
3、根据每个可见节点的内容和样式,生成渲染树Render Tree
二、回流重绘的概念
1、回流
概念:回流就是浏览器根据Render Tree在视口(viewport)计算节点具体位置和大小的过程。
为弄清每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。让我们考虑下面这样一个简单的实例:
以上代码包含两个嵌套div元素:父元素div将宽度设置为视口宽度的 50%,子元素div将其宽度设置为其父元素的50%,即视口宽度的25%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。如下图:
2、重绘
概念:根据渲染树和回流阶段,我们知道了可见节点的几何信息(位置、大小)和样式,那么我们就可以将渲染树的每个节点转换为屏幕上的实际像素,这个阶段就叫做重绘。
3、何时发生回流重绘
从以上我们知道了,回流阶段主要是计算节点的几何信息(位置、大小),那么当页面的布局和几何信息发生变化时,就会发生回流。比如:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
- 页面一开始渲染的时候(这肯定避免不了)
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
总结一句话就是:回流必定会引起重绘,重绘不一定会引起回流。
三、浏览器优化机制
为了提高性能,浏览器通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你使用了如下属性或方法:
offsetTop、offsetLeft、offsetWidth、offsetHeight
clientTop、clientLeft、clientWidth、clientHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
getComputedStyle()、getBoundingClientRect
以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。
四、如何避免、较少回流重绘
那么,我们如何避免或者减少回流重绘呢?
1、CSS
- 使用css的calss修改样式,尽可能在DOM树的最末端改变class
- 使用 visibility 替换 display: none,因为前者只会引起重绘,后者会引发回流
- 避免使用table布局
- 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
- 将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame
- CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
2、JavaScript
动态改变样式使用cssText
123456789// 不推荐写法const el = document.getElementById('box');el.style.padding = '4px';el.style.borderLeft = '2px';el.style.width = '50px';// 推荐写法:const el = document.getElementById('box');el.style.cssText += 'padding: 4px; border-left: 2px; width: 50px;';避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。