原文: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的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。