回头理解重绘和重排(Repaint & Reflow)
理解重绘和重排(Repaint & Reflow)
类似叫法
- 回流(Reflow)和重绘(Repaint)
个人比较倾向于 重绘和重排!
可以理解为在画画的时候,需要定框打形,确定你要画的元素内容在画布上的排列位置,比例等(排列)!
排列好之后,就可以具体去丰富你的画作!然后上色,最后完成你的画作(绘制)
那么我们的页面也是一样的!具体的浏览器解析渲染机制如下所示:
- 解析
HTML
,生成DOM
树,解析CSS
,生成CSSOM
树 - 将
DOM
树和CSSOM
树结合,生成渲染树(Render Tree
) Layout
(排列,回流):根据生成的渲染树,进行排列,回流(Layout
),得到节点的几何信息(位置,大小)Painting
(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素Display
:将像素发送给GPU,展示在页面上
重排:在页面初始渲染阶段,排列(回流)这一步是必不可少的,如果后面我们修改了画布上的一些元素,比如我们画人像画,定型的时候本来头是排列在左边,现在要排列到右边去,头占比也需要扩大一点,这种几何变化,会影响整体排列的,需要重新对整个画布的排列进行修改!我们就叫这一个操作为重排!
重绘:比如我们画的画现在到了上色阶段!上完了色。感觉鼻子的颜色或者头发的颜色想改一下,那么我们直接就可以将新的调色覆盖到我们原有的区域上!而不用重新去调整画中元素的布局!又或者画的人手上有个小首饰画的有点瑕疵,我不想要了,也可以对这个东西重新上色,让这个首饰的颜色和周围颜色融为一体,达到所谓透明的状态,让首饰消失掉!总之,这些不会影响整个画的布局的绘制操作!我们就称之为重绘!
从前端页面角度总结来说:
- 重绘:由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如
outline
,visibility
,color
,background-color
等。 - 重排:是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。
当我们对 DOM
的修改引发了 DOM
几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来
当我们对 DOM
的修改导致了样式的变化(color
或background-color
),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了重绘
重排(回流)触发时机
要想减少回流和重绘的次数,首先要了解回流和重绘是如何触发的
这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流,如下面情况:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
- 页面一开始渲染的时候(这避免不了)
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。
但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
- getBoundingClientRect
- 具体可以访问这个网站:https://gist.github.com/pauliri…
以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,**最好避免使用上面列出的属性,他们都会刷新渲染队列。**如果要使用它们,最好将值缓存起来。
重绘触发时机
触发重排(回流)一定会触发重绘
画纸上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(重排,回流),再画上它原有的颜色(重绘)
除此之外还有一些其他引起重绘行为:
- 颜色的修改
- 文本方向的修改
- 阴影的修改
如何减少
最小化重绘和重排
由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉。考虑这个例子
const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
例子中,有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。
当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。
但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排。
因此,我们可以合并所有的改变然后依次处理,比如我们可以采取以下的方式:
1.使用cssText
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
2.修改 class
const el = document.getElementById('test');
el.className += ' active';
批量修改DOM
当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:
第1步,使元素脱离文档流
第2步,对其进行多次修改
第3步,将元素带回到文档中。
该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。
有三种方式可以让DOM脱离文档流:
- 隐藏元素,应用修改,重新显示
- 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
- 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
考虑我们要执行一段批量插入节点的代码:
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
appendDataToElement(ul, data);
如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。
我们可以使用这三种方式进行优化:
1.隐藏元素,应用修改,重新显示
这个会在展示和隐藏节点的时候,产生两次重绘
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
ul.style.display = 'none'; // 先隐藏起来
appendDataToElement(ul, data);
ul.style.display = 'block'; // 然后放出来
2.使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
3.将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);
避免触发同步布局事件
上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。
举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。
这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:
const width = box.offsetWidth; // 先把这个缓存起来!
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
对于复杂动画效果,使用绝对定位让其脱离文档流
对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流
css3硬件加速(GPU加速)
比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!
使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
常见的触发硬件加速的css属性:
- transform
- opacity
- filters
- Will-change
css3硬件加速的坑
- 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
- 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。
评论区