更新记录
- 2023-09-18:对于“触发机制和优化手段”作了补充。
重绘和重排
阅读基础:掌握浏览器运作原理 | QT-7274 (qt7274.co)
-
当DOM结构中的元素发生改变,或者浏览器窗口大小改变,或者元素位置、大小、内容发生改变时,会重新进行样式计算(Style)、布局(Layout)、绘制(Paint)以及后面的所有流程,也就是整个页面都会重新渲染。这种行为我们称为重排。
-
当元素的样式(如颜色、背景、边框等)发生改变时,浏览器会重新绘制元素的外观,这个过程叫做重绘。重绘不会重新触发布局(Layout),但会触发样式计算(Style)和绘制(Paint),这种行为称为重绘。
-
区别:重排和重绘都会影响页面的渲染性能,但是重排的开销更大,因为它会重新计算元素的位置和大小,重新布局整个页面。而重绘只是重新绘制元素的外观,对于页面渲染性能的影响相对较小。因此,为了提高页面性能,尽量减少重排的次数,优化CSS样式,避免频繁操作DOM。
重排触发时机
重排这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要重排,如下面情况:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
- 页面一开始渲染的时候(这避免不了)
- 浏览器的窗口尺寸变化(因为重排是根据视口的大小来计算元素的位置和大小的)
还有一些容易被忽略的操作:获取一些特定属性的值
offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行重排
除此还包括getComputedStyle
方法,原理是一样的
重绘触发时机
触发重排一定会触发重绘
可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(重排),再画上它原有的颜色(重绘)
除此之外还有一些其他引起重绘行为:
- 颜色的修改
- 文本方向的修改
- 阴影的修改
浏览器优化机制
由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列
当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop
等方法都会返回最新的数据
因此浏览器不得不清空队列,触发重排重绘来返回正确的值
为什么要避免大量的重绘和重排?
重排和重绘都会占用主线程,同时JavaScript也会占用主线程,即他们会出现抢占执行时间的问题。
如果写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式计算布局和绘制的操作。如果在运行动画时还有大量的JavaScript代码,当在一帧的时间内布局和绘制结束后,还有剩余时间,JS就会拿到主线程的使用权。如果JS执行时间过长,就会导致在下一帧开始时JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面动画卡顿。
如何减少重排的手段
我们了解了如何触发重排和重绘的场景,下面给出避免重排的经验:
- 如果想设定元素的样式,通过改变元素的
class
类名 (尽可能在 DOM 树的最里层) - 避免设置多项内联样式
- 应用元素的动画,使用
position
属性的fixed
值或absolute
值(如前文示例所提) - 避免使用
table
布局,table
中每个元素的大小以及内容的改动,都会导致整个table
的重新计算 - 对于那些复杂的动画,对其设置
position: fixed/absolute
,尽可能地使元素脱离文档流,从而减少对其他元素的影响 - 使用css3硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起重排重绘 - 避免使用 CSS 的
JavaScript
表达式
在使用 JavaScript
动态插入多个节点时, 可以使用DocumentFragment
. 创建后一次插入. 就能避免多次的渲染性能
但有时候,我们会无可避免地进行重排或者重绘,我们可以更好使用它们
例如,多次修改一个把元素布局的时候,我们很可能会如下操作
const el = document.getElementById('el')
for(let i=0;i<10;i++) {
el.style.top = el.offsetTop + 10 + "px";
el.style.left = el.offsetLeft + 10 + "px";
}
每次循环都需要获取多次offset
属性,比较糟糕,可以使用变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求
// 缓存offsetLeft与offsetTop的值
const el = document.getElementById('el')
let offLeft = el.offsetLeft, offTop = el.offsetTop
// 在JS层面进行计算
for(let i=0;i<10;i++) {
offLeft += 10
offTop += 10
}
// 一次性将计算结果应用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop + "px"
我们还可避免改变样式,使用类名去合并样式
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
使用类名去合并样式
<style>
.basic_style {
width: 100px;
height: 200px;
border: 10px solid red;
color: red;
}
</style>
<script>
const container = document.getElementById('container')
container.classList.add('basic_style')
</script>
前者每次单独操作,都去触发一次渲染树更改(新浏览器不会),都去触发一次渲染树更改,从而导致相应的重排与重绘过程
合并之后,等于我们将所有的更改一次性发出。
我们还可以通过通过设置元素属性display: none
,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发重排与重绘,这个过程称为离线操作
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'
其他优化手段
- 通过
requestAnimationFrame()
这个API帮助我们解决这个问题,这个方法会在每一帧被调用,通过API的回调,我们可以把JS运行任务分成一些更小的任务块,在每一帧时间用完前暂停JS执行,归还主线程。这样在下一帧开始时,主线程可以按时执行布局和绘制。React的渲染引擎React Fiber就是用到了这个api来做了很多优化。 - 栅格化的整个流程是不占用主线程的,意味着它无需和JS抢夺主线程。而CSS中有个动画属性为
transform
,通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,所以不会受到主线程中JS执行的影响。且通过transform
实现的动画由于不需要经过布局绘制、样式计算等操作,节省了很多运算时间。