# 概念

回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)

重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。

TIP

重绘不一定导致回流,回流一定会导致重绘。

# 回流的触发条件

  • 改变 DOM 的几何属性,如 width、height、padding、margin、left、top、border 等等。
  • 改变 DOM 树的结构,如 DOM 节点的增、删、移动等。
  • 获取一些特定的属性值,如 offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight。因为这些属性的值都是通过即时计算才能得到,所以获取这些值也会触发回流。

# 重绘的触发条件

以上触发回流的操作都会触发重绘制,除此之外修改背景、颜色、通过 visibility: hidden 隐藏一个 DOM 元素等也会触发重绘。

# 优化办法

# 避免频繁改动 DOM

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    div {
      width: 100px;
      height: 100px;
      background-color: red;
      position: absolute;
    }
  </style>
</head>
<body>
  <div id="box"></div>
  <script>
    const box = document.getElementById('box');
    for (let i = 0; i < 10; i++) {
      box.style.left = box.offsetLeft + 10 + 'px';
    }
  </script>
</body>
</html>

下面这样可以减少不必要的 DOM 操作。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    div {
      width: 100px;
      height: 100px;
      background-color: red;
      position: absolute;
    }
  </style>
</head>
<body>
  <div id="box"></div>
  <script>
    const box = document.getElementById('box');
    let offsetLeft = box.offsetLeft;

    for (let i = 0; i < 10; i++) {
      offsetLeft += 10;
    }
    box.style.left = offsetLeft + 'px';
    
  </script>
</body>
</html>

# DocumentFragment

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    const fragment = document.createDocumentFragment()
    
    for (let i = 0; i < 5; i++) {
      const span = document.createElement('span');
      span.innerHTML = i;

      fragment.appendChild(span)
      // document.body.appendChild(span)
    }
    document.body.appendChild(fragment);
  </script>
</body>
</html>

# 使用类合并修改样式

const box = document.getElementById('box');
box.style.width = '200px';
box.style.height = '200px';
box.style.border = '1px solid red';
box.style.padding = '10px';

上面这样对 box 进行了多次操作,会频繁触发回流,我们可以用一个类来合并这些修改,降低 DOM 操作。

.box {
  width: 200px;
  height: 200px;
  border: 1px solid red;
  padding: 10px;
}
const box = document.getElementById('box');
box.classList.add('box')

# 将 DOM “离线”

回流和重绘都发生在当前被操作的 DOM 在页面上,如果我们在操作前先把这个 DOM 从页面下掉,等操作完再将这个 DOM 显示到页面上,这样也能减少回流和重绘制。但是因为涉及到 DOM 的显示和隐藏,肯定也会发生回流,所以这种方式并不适合频繁操作 DOM 的情况。

const box = document.getElementById('box');
box.style.display = 'none';

box.style.width = '200px';
box.style.height = '200px';
box.style.color = 'orange';

box.style.display = 'block';

现代浏览器都比较智能,很多之前需要使用的优化目前不用也是没问题的。不过为了使项目在不同的浏览器都能获得不错的运行效果,能优化的优化下挺好的,毕竟我们要精益求精嘛。

# 参考文章

浏览器层合成与页面渲染优化 (opens new window)

最后一击——回流(Reflow)与重绘(Repaint) (opens new window)