原文:Browser | 重绘和回流
前言:回流(重排)和重绘是前端面试过程中经常会提到的一个知识点,所以想写一篇文章整理下相关的知识点。
要了解什么是回流(重排)与重绘,首先就要了解一下浏览器是如何进行画面渲染的。
浏览器是如何进行画面渲染的?
- 解析(Parser)HTML,生成DOM树(Tree),解析(Parser)CSS,生成样式规则(Style Rules)
- 根据刚才生成的DOM树与样式规则,生成渲染树(Render Tree)
- 进行布局Layout(回流/重排):根据生成的渲染树,得到节点的集合信息(位置,大小)
- 进行绘制Painting(重绘):根据计算和获取的信息进行整个页面的绘制
- Display:最后将画面显示在页面上
重绘和回流(重排)
- 回流:渲染树(Render Tree)中的元素的尺寸、结构、布局(几何属性)发生改变的时候,浏览器就会重新渲染部分或者全部文档的过程。
- 重绘:对元素样式的改变并不影响它在文档流中的位置和文档布局(没有改变元素的几何属性),浏览器直接为该元素绘制新的样式。
回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流。
如何触发
回流(重排)
-
页面一开始加载的时候(无法避免)
-
脚本操作DOM(增加或者删除可见的DOM元素)
-
元素的几何属性发生变化(大小,位置)
-
元素的内容发生了变化(如:input框中输入的内容,图片被另一个不同尺寸的图片所替代)
-
激活css伪类(如:
#div::hover
) -
字体的大小发生改变
-
浏览器的窗口大小发生了改变(回流是根据视口的大小来计算元素的位置和大小的)
-
使用一些特定的属性
offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
这些属性是通过实时计算得到的,浏览器获取这些值的时候,也会进行回流的操作,使用
getComputedStyle
方法的时候同理
大部分时候可以认为,只要影响到页面布局就会有回流的发生。
重绘
- 上文提到的回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流。
- 颜色的修改
- 文本方向的修改
- 阴影的修改
下面来看几个和重绘与回流相关的案例熟悉一下。
案例1:
javascript
var s = document.body.style;
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 再一次重绘
s.backgroundColor = "#ccc"; // 再一次 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
// 添加node,再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));
案例2:
问题:display:none
和visibility:hidden
会产生回流与重绘吗?
答:
-
display:none
元素隐藏之后不占用文档流(在渲染树里面不存在节点),DOM树发生了变化,所以会引起重绘与回流。 -
visibility:hidden
显示在页面上,但是隐藏元素仍需占用与未隐藏时一样的空间(在渲染树里面存在节点),没有影响到页面的结构,所以只有产生重绘。
补充:display: none
的子元素不会进行显示,而visibility: hidden
的子元素却是可以进行设置显示的
浏览器优化机制
由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。
浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列.
当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop
等方法都会返回最新的数据
因此浏览器不得不清空队列,触发回流重绘来返回正确的值。
如何避免触发回流与重绘
- 如果想通过js修改样式最好通过类(
class
)的方式去触发(补充:使用cssText也可以) - 避免设置多项内联样式
- 批量修改dom时候通过以下思路减少回流与重绘的发生
- 使元素脱离文档流
- 对其进行多次修改
- 将元素带回到文档中
- 避免触发同步布局事件,比如前文提到的需要实时读取的属性(如:
offsetWidth
),这样不需要每次循环的时候都读取一次 - 遇到复杂的动画效果,使用
position: fixed/absolute
让其脱离文档流,从而减少对其他元素的影响 - css3硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起回流重绘。- 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
- 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。
设定元素样式,最好通过类(class
)的方式去触发
下面这种方式,在比较老的浏览器上每次赋值操作都会引起回流与重绘
❕注:比较新的浏览器会使用队列来储存多次修改,进行优化,所以在新的浏览器上只会触发一次重绘和回流
javascript
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
为了避免触发重回与回流,可以使用类(class
)来给元素触发对应的样式,下面就是优化过后的代码:
CSS:
css
.active {
width: 100px;
height: 200px;
border: 10px solid red;
color: red;
}
JS:
javascript
const el = document.querySelector('#container');
// 给元素追加一个类
el.classList.add('active');
补充:其实还可以使用cssText
来给样式做重新赋值,但是这种方式写法看上去比较繁琐,所以了解一下就好了。对应的代码如下:
javascript
const el = document.querySelector('#container');
// 通过cssText属性给元素添加样式
el.style.cssText = 'width: 100px;' +
'height: 200px;' +
'border: 10px solid red;' +
'color: red;';
批量修改dom时候通过以下思路减少回流与重绘的发生
- 使元素脱离文档流
- 对其进行多次修改
- 将元素带回到文档中
该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。
有三种方式可以让DOM脱离文档流:
- 隐藏元素,应用修改,重新显示
- 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
- 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
隐藏元素,应用修改,重新显示
javascript
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';
将ul
的display
设置为none
后,该元素就不存在与渲染树中了
使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档
javascript
const ul = document.getElementById('list');
// 使用createDocumentFragment创建一个新的空白的文档片段
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
// 将元素追加到ul元素中去
ul.appendChild(fragment);
DocumentFragments (en-US) 是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。
因为文档片段存在于内存中,并不在 DOM 树中 ,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。------(MDN-Document.createDocumentFragment())
将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
javascript
const ul = document.getElementById('list');
// 对该节点进行克隆,参数1为true代表深度克隆
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
// 将旧的元素替换为修改后的元素
ul.parentNode.replaceChild(clone, ul);
避免触发同步布局事件
javascript
// 将变量放在循环的外面,避免每次循环的时候都要重新读取,进而导致没必要的重绘与回流发生
const width = box.offsetWidth;
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
css3硬件加速(GPU加速)
划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
常见的触发硬件加速的css属性:
- transform
- opacity
- filters
- Will-change
注意:
- 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
- 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。