Repaint(回流与重绘)

138 阅读5分钟

渲染过程基础

当我们在浏览器中输入URL后,浏览器会经历以下关键步骤来渲染页面:

  1. 下载HTML文件并解析成DOM树

  2. 下载CSS文件并解析成CSSOM树

  3. 将DOM和CSSOM合成渲染树(RenderTree)

  4. 生成布局树(Layout),计算元素位置和大小

  5. 创建图层,处理堆叠上下文

  6. 合并图层

  7. 光栅化,将图层转换为像素点

这个过程中,回流和重绘是两个重要且常被讨论的性能瓶颈。

推荐大家可以理解一下下面该图的作用。

image.png


什么是BFC与文档流

在理解回流重绘前,需要明确文档流的概念:

  • BFC(Block Formatting Context):块级格式化上下文,规定了块级元素从上到下排列
  • HTML根元素天然形成第一个BFC
  • 触发BFC的常见方式:floatoverflow:hiddendisplay:flex

页面怎样渲染的

image.png



字节面试题

让我们来看下字节的面试题,下面代码会发生几次回流几次重绘。

    <script>
      let el = document.getElementById("app");
      el.style.width = el.offsetWidth + 1 + "px";
      el.style.width = 1 + "px";
    </script>

当我们看过去是不是脑海中冒出两次回流,两次重绘❓❓❓我们看到el.style.width = el.offsetWidth + 1 + "px";这里发生了一次回流,el.style.width = 1 + "px";同理这里修改了元素的属性又一次发生了回流,这里大家很容易就觉得是两次但是大厂的问题哪会如此简单。让我们来通过下一段代码来深度理解一下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .box {
        width: 100px;
        height: 100px;
        background: #000;
      }
    </style>
  </head>
  <body>
    <div class="box"></div>
    <script>
      let box = document.querySelector(".box");

      box.addEventListener("click", function () {
        box.style.width = 200 + "px";
        box.style.width = 300 + "px";
        box.style.width = 400 + "px";
        box.style.width = 50 + "px";
      });
    </script>
  </body>
</html>

效果图:当我们点击之后可以看到宽度直接变成了50px。直接展示给用户可以看到是最后给方块的赋值为50px。上述代码理论上会发生四次回流,但是实际只会发生一次回流。浏览器内置了渲染队列,当浏览器执行到一个需要回流的操作之后,会先将该操作放入队列,继续往下执行,如果下面还有导致回流的行为,就一直入队列,直到队列达到阈值或者后面没有新的回流行为,才会将渲染队列中的行为一次性执行,并一次性修改样式。

image.png

让我们回到字节的面试题当中,这里大家是不是知道现在发生了一次回流与重绘呀🌹🧡🌹

    <script>
      let el = document.getElementById("app");
      el.style.width = el.offsetWidth + 1 + "px";
      el.style.width = 1 + "px";
    </script>

答案确实是一次回流一次重绘,但是这样子回答给面试官只能得到一半的分数。让我们重新回到上述的代码,打印出每次修改宽度属性的值看看输出的结果!!!

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .container {
        display: flex;
      }
      .box1 {
        width: 100px;
        height: 100px;
        background: pink;
      }
      .box2 {
        width: 100px;
        height: 100px;
        background: pink;
        margin-left: 150px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="box1"></div>
      <div class="box2"></div>
    </div>
    <script>
      let box = document.querySelector(".box2");

      box.addEventListener("click", function () {
        console.log(box.offsetWidth);

        box.style.width = 200 + "px";
        console.log(box.offsetWidth);

        box.style.width = 300 + "px";
        console.log(box.offsetWidth);

        box.style.width = 400 + "px";
        console.log(box.offsetWidth);

        box.style.width = 50 + "px";
        console.log(box.offsetWidth);
      });
    </script>
  </body>
</html>

可以看到结果界面:offsetWidth会强制渲染队列的属性。在下述代码当中可以得知页面就会发生四次回流,clientxxx,scrollxxx开头的属性都会导致渲染队列重新执行。

image.png

让我们再重新回到字节的面试题:只会回流一次的原因,el.offsetWidth + 1 + "px";先执行代码的右侧代码先读取容器的宽度,读取容器的宽度就会导致渲染队列强制清空掉,这时就会把渲染队列当中的回流行为拿出来执行,但是不巧的是渲染队列当中没有容器的宽度。因此在这段代码当中,只发生了一次的回流与重绘。

<script>
      let el = document.getElementById("app");
      el.style.width = el.offsetWidth + 1 + "px";
      el.style.width = 1 + "px";
 </script>


减少回流

1.先让元素脱离文档流,再修改几何属性,再放回文档流

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .container {
        display: flex;
      }
      .box1 {
        width: 100px;
        height: 100px;
        background: pink;
      }
      .box2 {
        width: 100px;
        height: 100px;
        background: pink;
        margin-left: 150px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="box1"></div>
      <div class="box2"></div>
    </div>
    <script>
      let box = document.querySelector(".box2");

      box.addEventListener("click", function () {
        box.style.display = "none";
        console.log(box.offsetWidth);

        box.style.width = 200 + "px";
        console.log(box.offsetWidth);

        box.style.width = 300 + "px";
        console.log(box.offsetWidth);

        box.style.width = 400 + "px";
        console.log(box.offsetWidth);

        box.style.width = 50 + "px";
        console.log(box.offsetWidth);

        box.style.display = "block";
      });
    </script>
  </body>
</html>

可以看到效果和控制台的打印数据:此时只会发生两次回流,消失的那一刻会发生一次,出现的那一次会发生回流。

image.png

2.使用虚拟文档

    <ul class="list"></ul>

    <script>
      const list = document.querySelector(".list");
      const items = ["apple", "banana", "cherry", "cheese"];

      for (let i = 0; i < items.length; i++) {
        const li = document.createElement("li");
        li.textContent = items[i];
        list.appendChild(li);
      }
    </script>

可以看到上次代码的回流次数为4次,需要在此处减少回流的次数优化性能如何实现呢❓❓❓通过添加虚拟片段,创建文档碎片,在虚拟文档中修改添加属性。

    <ul class="list"></ul>

    <script>
      const list = document.querySelector(".list");
      const items = ["apple", "banana", "cherry", "cheese"];

      // 文档碎片
      let frg = document.createDocumentFragment();

      for (let i = 0; i < items.length; i++) {
        const li = document.createElement("li");
        li.textContent = items[i];
        frg.appendChild(li);
      }
      list.appendChild(frg);
    </script>

3.使用克隆DOM 创建 ul 元素的深拷贝,即复制 ul 元素及其所有子元素,减少回流的行为

      const cloneUl = ul.cloneNode(true);
      for (let i = 0; i < items.length; i++) {
        const li = document.createElement("li");
        li.textContent = items[i];
        cloneUl.appendChild(li);
      }
      list.parentNode.replaceChild(cloneUl, ul);


总结

回流,重点在于“流”,倾向于结构调整,对性能影响更大(房子重新盖就是回流)

重绘,重点在于“绘”,倾向于样式调整,对性能影响较小(房子重新装修就是重绘)

触发回流的常见操作

  • 修改元素的尺寸( width 、 height 等)或位置( margin 、 padding 等)。
  • 添加或删除可见的 DOM 元素。
  • 修改字体大小。
  • 改变浏览器窗口大小。
  • 查询某些属性或调用某些方法,如 offsetTop 、 offsetLeft 、 scrollTop 等。

减少回流方式

image.png