在HTML
中,每个元素都可以理解成一个盒子,在浏览器解析过程中,会涉及到回流与重绘:
回流(重排):布局引擎会根据各种样式计算每个盒子在页面上的大小与位置
重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个盒子特性进行绘制
具体的浏览器解析渲染机制如下所示:
解析HTML,生成DOM树,解析CSS,生成CSSOM树
将DOM树和CSSOM树结合,生成渲染树(Render Tree)
Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
Display:将像素发送给GPU,展示在页面上
在页面初始渲染阶段,回流不可避免的触发,可以理解成页面一开始是空白的元素,后面添加了新的元素使页面布局发生改变
当我们对 DOM
的修改引发了 DOM
几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来
当我们对 DOM
的修改导致了样式的变化(color
或background-color
),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了重绘
要想减少回流和重绘的次数,首先要了解回流和重绘是如何触发的
回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流,如下面情况:
还有一些容易被忽略的操作:获取一些特定属性的值
offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流
除此还包括getComputedStyle
方法,原理是一样的
触发回流一定会触发重绘
可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)
除此之外还有一些其他引起重绘行为:
颜色的修改
文本方向的修改
阴影的修改
由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列
当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop
等方法都会返回最新的数据
因此浏览器不得不清空队列,触发回流重绘来返回正确的值
我们了解了如何触发回流和重绘的场景,下面给出避免回流的经验:
class
类名 (尽可能在 DOM 树的最里层)position
属性的 fixed
值或 absolute
值(如前文示例所提)table
布局,table
中每个元素的大小以及内容的改动,都会导致整个 table
的重新计算position: fixed/absolute
,尽可能地使元素脱离文档流,从而减少对其他元素的影响transform
、opacity
、filters
这些动画不会引起回流重绘JavaScript
表达式在使用 JavaScript
动态插入多个节点时, 可以使用DocumentFragment
. 创建后一次插入. 就能避免多次的渲染性能
但有时候,我们会无可避免地进行回流或者重绘,我们可以更好使用它们:
1、避免改变样式,使用类名去合并样式:
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
改成:
前者每次单独操作,都去触发一次渲染树更改(新浏览器不会),
都去触发一次渲染树更改,从而导致相应的回流与重绘过程
合并之后,等于我们将所有的更改一次性发出
2、将 DOM “离线”
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了许多类似的后续操作)
改成:
let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了许多类似的后续操作)
container.style.display = 'block'
有的同学会问,拿掉一个元素再把它放回去,这不也会触发一次昂贵的回流吗?这话不假,但我们把它拿下来了,后续不管我操作这个元素多少次,每一步的操作成本都会非常低。当我们只需要进行很少的 DOM 操作时,DOM 离线化的优越性确实不太明显。一旦操作频繁起来,这“拿掉”和“放回”的开销都将会是非常值得的。
参考文献:
回流与重绘 - 掘金