在前端面试中,回流与重绘是经常被提及的概念,因为它们直接关系到页面性能的优化。比如会问到什么情况下会发生回流与重绘?如何优化回流和重绘?今天就让我们来深入探讨一下回流与重绘,帮助你在前端面试中提及相关问题更好地应对。
一. 浏览器的渲染过程
理解回流与重绘之前,我们需知道当我们在浏览器中输入一个网址或点击一个链接时,浏览器会先进行资源请求,浏览器将网址解析成服务器网址,通过ip地址建立TCP连接,发送HTTP请求资源,接收响应然后下载服务器返回的资源。
资源请求完了后,浏览器就开始渲染网页了,让我们来着重看下浏览器的渲染过程:
- 解析数据包得到 html,css文件 (将二进制的字节数据解析成字符串)
- 字节数据 =》 字符串 =》 Token =》 Node节点 =》 构建DOM 树
- css 文件转成 CSSOM 树
- DOM + CSSOM == render 树 (不可见的元素是不会出现在 render 树中)
- 计算布局 (回流) ---- 很耗费性能的
- GPU 绘制 (重绘)
其中Token代表html标签属性等,然后Token被转化为Node节点,每个节点代表html里面的一个元素,然后构建DOM树,css构成CSSOM树,然后浏览器将DOM树和CSSOM树结合,生成渲染树(render 树)。
回流是计算渲染树中各个节点的几何信息的过程,即它们的位置和尺寸,重绘是在回流完成后,浏览器将渲染树的每个节点绘制到屏幕上的过程。
二. 回流与重绘
回流
浏览器根据 CSS 样式计算布局,也就是元素的大小和位置。
以下操作会发生回流;
- 页面初次渲染
- 窗口大小改变
- 增加,删除可见的 DOM 元素
- 改变元素的几何信息
重绘
发生在回流之后,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)。
- offsetxxx (还有Parent)
- clientxxx(不包含边框)
- 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>
- 先让元素脱离文档,再修改几何属性,再放回文档
前面说到不可见的元素是不会出现在 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>
只发生俩次回流,脱离文档时发生一次,放入文档时发生一次。
- 使用虚拟文档
<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
中。
- 使用克隆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 元素时发生一次回流。
五. 总结
-
理解渲染过程:浏览器的渲染过程包括解析HTML和CSS,构建DOM树和CSSOM树,然后生成渲染树,接着计算布局(回流),最后进行GPU绘制(重绘)。
-
回流与重绘的区别:回流是计算元素的位置和大小,而重绘是绘制元素的外观。回流必然会导致重绘,但重绘不一定引发回流。
-
浏览器的优化:内置一个渲染队列,会将所有回流任务放入渲染队列后一次性全部执行。
-
减少回流:
- 先让元素脱离文档,再修改几何属性,再放回文档
- 使用虚拟文档
- 使用克隆dom
好了,关于面试中的回流与重绘就介绍到这里了,如果觉得对你有所帮助可以点点赞哦🤗。