前端面试:回流与重绘

375 阅读6分钟

在前端面试中,回流与重绘是经常被提及的概念,因为它们直接关系到页面性能的优化。比如会问到什么情况下会发生回流与重绘?如何优化回流和重绘?今天就让我们来深入探讨一下回流与重绘,帮助你在前端面试中提及相关问题更好地应对。

一. 浏览器的渲染过程

理解回流与重绘之前,我们需知道当我们在浏览器中输入一个网址或点击一个链接时,浏览器会先进行资源请求,浏览器将网址解析成服务器网址,通过ip地址建立TCP连接,发送HTTP请求资源,接收响应然后下载服务器返回的资源。

资源请求完了后,浏览器就开始渲染网页了,让我们来着重看下浏览器的渲染过程

  1. 解析数据包得到 html,css文件 (将二进制的字节数据解析成字符串)
  2. 字节数据 =》 字符串 =》 Token =》 Node节点 =》 构建DOM 树
  3. css 文件转成 CSSOM 树
  4. DOM + CSSOM == render 树 (不可见的元素是不会出现在 render 树中)
  5. 计算布局 (回流) ---- 很耗费性能的
  6. GPU 绘制 (重绘)

其中Token代表html标签属性等,然后Token被转化为Node节点,每个节点代表html里面的一个元素,然后构建DOM树,css构成CSSOM树,然后浏览器将DOM树和CSSOM树结合,生成渲染树(render 树)。

回流是计算渲染树中各个节点的几何信息的过程,即它们的位置和尺寸,重绘是在回流完成后,浏览器将渲染树的每个节点绘制到屏幕上的过程。

二. 回流与重绘

回流

浏览器根据 CSS 样式计算布局,也就是元素的大小和位置。

以下操作会发生回流;

  1. 页面初次渲染
  2. 窗口大小改变
  3. 增加,删除可见的 DOM 元素
  4. 改变元素的几何信息

重绘

发生在回流之后,GPU 绘制,进行图形处理,改变元素的非几何信息(比如背景颜色等)都会发生重绘。

回流一定重绘,重绘不一定回流:比如当你改变元素位置和大小的的时候,其身上的样式(如背景颜色)肯定会跟着渲染一定重绘,当重绘时其布局不一定会改变,也就不一定发生回流。

既然知道了回流与重绘,那我们来看一道字节的面试题:

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

问发生了几次回流与重绘?一看改变了两次el元素的宽,也就会发生两次回流与重绘,但是既然是字节的面试题,当然不会这么浅了,正确答案是一次,接下来就来看下为什么?

三. 浏览器的优化

回流很耗费性能,浏览器内置了渲染队列,当浏览器执行到一个需要回流的操作之后,会先将该操作放入队列,继续往下执行,如果下面还有导致回流的行为,就一直入队列,直到队列达到阈值(最大值)或者后面没有新的回流行为,才会将渲染队列中的行为一次性执行,并一次性修改样式。

比如以下代码;

<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>

在回调函数中,.box元素的width样式被连续更改四次,浏览器将这些样式更改操作放入渲染队列中,然后接着执行下面代码,执行完后再将渲染队列中的任务一次性执行完,这样就可以减少不必要的回流,减少性能的开销。

但是有些强制执行渲染队列的属性(下面的xxx有Top,Left,Width,Height)。

  1. offsetxxx (还有Parent)
  2. clientxxx(不包含边框)
  3. scrollxxx

当你读取这些属性时,浏览器需要确保返回的是最新的布局信息,因此它会立即执行回流来计算这些值。

然后我们再来看这道面试题;

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

第一行只是获取元素,不会回流,第二行等号先执行右边的el.offsetWidth,强制执行渲染队列,但此时渲染队列中没有任务,然后执行等号左边等于右边(el.offsetWidth + 1) + 'px'宽度改变发生回流,先不执行,将它放入渲染队列中,继续执行第三行,宽度变为1px,发生回流再放入渲染队列中,然后将渲染队列中的任务一次性执行,所以只会发生一次回流与重绘。

四. 减少回流

当面试时候提到回流,面试官基本上都会随口问一句,如何减少回流呢?

以下面代码为例,每创建一个li加入ul就会回流一次;

<script>
  const ul = document.querySelector('.list')
  const items = ['apple', 'banana', 'cherry', 'cheese', 'citrus', 'Dates']
  
  for (let i = 0; i < items.length; i++) {
    const li = document.createElement('li')
    li.textContent = items[i]
    ul.appendChild(li)
  }
</script>
  1. 先让元素脱离文档,再修改几何属性,再放回文档

前面说到不可见的元素是不会出现在 render 树中,因此它们不会发生回流与重绘。

<script>
  const ul = document.querySelector('.list')
  const items = ['apple', 'banana', 'cherry', 'cheese', 'citrus', 'Dates']

  ul.style.display = 'none'   //脱离文档,不可见

  for (let i = 0; i < items.length; i++) {
    const li = document.createElement('li')
    li.textContent = items[i]
    ul.appendChild(li)
  }

  ul.style.display = 'display'   //放回文档,可见
  
</script>

只发生俩次回流,脱离文档时发生一次,放入文档时发生一次。

  1. 使用虚拟文档
<script>
  const ul = document.querySelector('.list');
  const items = ['apple', 'banana', 'cherry', 'cheese', 'citrus', 'Dates'];

  let frg = document.createDocumentFragment(); // 创建一个虚拟文档

  for (let i = 0; i < items.length; i++) {
    const li = document.createElement('li'); 
    li.textContent = items[i]; 
    
    frg.appendChild(li); // 将 li 元素添加到文档中
  }

  ul.appendChild(frg); // 将文档一次性添加到 ul 元素中,减少回流和重绘的次数
</script>

只发生最后的虚拟文档一次性添加到 ul 元素中这一次回流。相当于建一个盒子装起全部li,再一次性加入到ul中。

  1. 使用克隆dom
<script>
  const ul = document.querySelector('.list');
  const items = ['apple', 'banana', 'cherry', 'cheese', 'citrus', 'Dates'];

  // 克隆原始的 ul 元素
  const cloneUl = ul.cloneNode(true);
  
  // 循环添加新的 li 元素到克隆的 ul 元素中
  for (let i = 0; i < items.length; i++) {
    const li = document.createElement('li');
    li.textContent = items[i];
    cloneUl.appendChild(li);
  }
  
  // 用克隆的 ul 元素替换原始的 ul 元素
  ul.parentNode.replaceChild(cloneUl, ul);
</script>

原理跟虚拟文档相似,克隆原始的 ul 元素,在它身上添加li,添加完后再替换原始的 ul 元素,只发生克隆的 ul 元素替换原始 ul 元素时发生一次回流。

五. 总结

  1. 理解渲染过程:浏览器的渲染过程包括解析HTML和CSS,构建DOM树和CSSOM树,然后生成渲染树,接着计算布局(回流),最后进行GPU绘制(重绘)。

  2. 回流与重绘的区别:回流是计算元素的位置和大小,而重绘是绘制元素的外观。回流必然会导致重绘,但重绘不一定引发回流。

  3. 浏览器的优化:内置一个渲染队列,会将所有回流任务放入渲染队列后一次性全部执行。

  4. 减少回流

  • 先让元素脱离文档,再修改几何属性,再放回文档
  • 使用虚拟文档
  • 使用克隆dom

好了,关于面试中的回流与重绘就介绍到这里了,如果觉得对你有所帮助可以点点赞哦🤗。