回流和重绘操作
在面试中,回流和重绘是经常被面试官提及的问题。本文将带您深入浅出地探讨回流和重绘的相关概念,并介绍一些优化策略。
基本概念
浏览器资源处理流程
为了理解回流和重绘的流程,首先需要了解浏览器生成页面的步骤:
-
解析HTML和CSS: 浏览器获取资源后,解析HTML生成DOM树,解析CSS生成CSSOM树。
-
生成Render Tree: 将DOM树和CSSOM树合并,去除不可见元素,生成Render Tree,为页面布局和绘制做准备。
-
计算布局和绘制页面: 根据Render Tree进行布局计算,得到每个节点的几何信息,并利用GPU进行页面绘制。
回流和重绘的概念
回流
-
定义: 浏览器为了重新渲染部分或全部文档(仅包含文档流中的元素),而重新计算元素的位置和几何结构的过程。
-
触发条件: 当页面布局发生变化,如改变窗口尺寸、元素大小、增加或删除可见元素,以及页面初次渲染时,浏览器需要进行回流,以确保页面准确展示。
重绘
-
定义: 重绘是指在元素样式发生变化,但不影响其布局的情况下,浏览器重新绘制元素的过程。这意味着元素的位置和大小没有改变,仅仅是样式的外观发生了变化。
-
触发条件: 修改背景颜色、背景图片、边框颜色、字体颜色等。
对比
回流的过程相对昂贵,因为它会触发渲染树的重新构建和重新布局,可能导致性能下降。相对于回流而言,重绘的开销较小,因为它不涉及重新计算元素的布局信息。因此,为了提高性能,应尽量减少回流的发生,通过合理的优化策略避免不必要的页面布局计算 。注意:重绘的发生并不一定导致回流,但回流发生必定会触发重绘 在优化网页性能时,也要尽量减少不必要的重绘操作,特别是在需要频繁更新样式的情况下。
优化
回流的花费更高,我们往往会有意使用一些方式来避免多次回流
浏览器的优化策略
现代浏览器采用渲染队列机制,将样式变更操作进入队列,批量执行以减少性能开销。也就是说,当许多操作需要回流时,先进入队列,最后一起处理操作,多次回流优化成为一次回流。但是一些属性和方法如offsetWidth、scrollHeight可能导致强制执行渲染队列所有任务,需注意使用。
下面的代码演示了浏览器优化回流的原理:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">hello</div>
<script>
let app = document.getElementById("app");
app.style.position = "relative";
// 一次回流
app.style.height = "100px";
app.style.width = "200px";
app.style.left = "10px";
app.style.top = "10px";
console.log(app.offsetLeft);
console.log(app.offsetWidth);
console.log(app.offsetTop);
console.log(app.offsetHeight);
</script>
</body>
</html>
在这个例子中,通过一次性设置多个样式,减少了回流的次数,提高了性能。
下面这个例子将展示特殊属性强制执行渲染任务队列导致多次回流
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="app">hello</div>
<script>
let app = document.getElementById("app");
app.style.position = "relative";
// 导致5次回流,开销过大(微秒级)
app.style.height= '100px'
console.log(app.offsetHeight);//迫使浏览器渲染队列强制刷新
app.style.width ='200px'
console.log(app.offsetWidth);
app.style.left = '10px'
console.log(app.offsetLeft);
app.style.top = '10px'
console.log(app.offsetTop);
</script>
</body>
</html>
优化策略:避免回流
深入了解渲染队列机制: 确保元素样式变更的批量执行,减少回流次数。
巧妙利用display:none: 在修改样式前,将元素样式设置为display:none,再修改完毕后恢复为display:block,以防止渲染队列溢出。
1. 使用display:none来优化性能。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Display None 优化示例</title>
<style>
.hidden {
display: none;
}
</style>
</head>
<body>
<div id="content">
<div id="element1">元素 1</div>
<div id="element2">元素 2</div>
<div id="element3">元素 3</div>
</div>
<script>
const content = document.getElementById('content');
const element1 = document.getElementById('element1');
const element2 = document.getElementById('element2');
const element3 = document.getElementById('element3');
// 使用 "hidden" 类将容器隐藏,以优化样式修改
content.classList.add('hidden');
// 修改样式而不频繁触发回流
element1.style.backgroundColor = 'red';
element2.style.fontSize = '18px';
element3.style.width = '150px';
// 确保在容器重新可见之前不会触发回流
content.classList.remove('hidden');
</script>
</body>
</html>
在这个例子中,回流被最小化,因为它仅在隐藏和显示容器时发生,而不是在每次修改样式后都触发。
我们也可以利用这个类似的原理通过js中已有的特殊变量存储页面元素的变化,之后一次性放入页面中来实现减少回流。
2. 不使用 Fragment 优化性能
const ul = document.getElementById('box');
ul.style.display = 'none';
for (let i = 0; i < 100; i++) {
let li = document.createElement('li');
let text = document.createTextNode(i);
li.appendChild(text);
ul.appendChild(li);
}
ul.style.display = 'block';
这段代码中,我们在循环中直接将li元素追加到ul中。在每次追加元素时,都会引发回流,因为每个新的li元素的添加都会改变ul的布局。
3. 使用 Fragment 优化性能
const ul = document.getElementById('box');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
let li = document.createElement('li');
let text = document.createTextNode(i);
li.appendChild(text);
fragment.appendChild(li);
}
ul.appendChild(fragment);
在这里,我们使用了文档片段(Fragment)来优化性能。通过将所有li元素添加到文档片段中,再将文档片段一次性追加到ul,我们减少了回流的次数。文档片段在内存中进行操作,并没有存在文档流中,因此不会触发实际的渲染。
4. 使用 cloneNode 优化性能
const ul = document.getElementById('box');
const clone = ul.cloneNode(true);
for (let i = 0; i < 100; i++) {
let li = document.createElement('li');
let text = document.createTextNode(i);
li.appendChild(text);
clone.appendChild(li);
}
ul.parentNode.replaceChild(clone, ul);
在这里,我们使用cloneNode方法来创建ul的副本,然后向副本中添加li元素。类似上面的文档片段,添加的li节点添加到副本上而没有直接添加到页面中,不造成回流。最后,我们用副本替换原始的ul,这样可以减少回流次数。但要注意,cloneNode会复制整个节点,包括其子节点和属性。
字节面试题
以下代码会回流多少次
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
let el = document.getElementById('app');
el.style.width = (el.offsetWidth + 1) + 'px';
el.style.width = '1px';
</script>
</body>
</html>
这段代码会触发两次回流。
-
第一次回流是由于以下代码:
let el = document.getElementById('app'); el.style.width = (el.offsetWidth + 1) + 'px';这里通过
offsetWidth获取元素宽度虽然会执行渲染列表中的任务队列,但是此时队列为空,所以并不会发生回流。但是这里又设置了新的宽度,这会导致浏览器执行回流。 -
第二次回流是由于以下代码:
el.style.width = '1px'; // 触发重绘在这里,修改了元素的宽度属性,导致浏览器进行重绘,进而触发了第二次回流。
当页面元素的宽度被修改,即使修改的值与原来相同,也会触发回流。回流是浏览器重新计算元素的位置和几何结构的过程,而不仅仅是样式的变化。因此,如果你改变了元素的宽度,即使新的宽度值与原来的相同,浏览器仍然会触发回流,重新计算元素的布局。这可能会导致性能损失,特别是在频繁触发回流的情况下。
总结
-
回流的定义: 浏览器为了重新渲染部分或全部文档而重新计算元素的位置和几何结构的过程。触发条件包括页面布局变化、窗口尺寸改变、元素大小变化、增删可见元素等。
-
重绘的定义: 在元素样式发生变化但不影响布局的情况下,浏览器重新绘制元素的过程。触发条件包括修改背景颜色、背景图片、边框颜色、字体颜色等。
-
优化策略: 为了提高性能,应尽量减少回流的发生。通过深入了解渲染队列机制,确保元素样式变更的批量执行,减少回流次数。巧妙利用
display:none,在修改样式前将元素样式设置为隐藏,再修改完毕后恢复显示,以防止渲染队列溢出。 -
其他优化手段: 使用文档片段(Fragment)或
cloneNode来优化性能,减少回流次数。深入了解浏览器的优化策略,如渲染队列机制,可以更好地掌握回流和重绘的优化方法。 -
注意事项: 即使修改后的宽度值与原来相同,改变元素宽度仍然会触发回流。