渲染过程基础
当我们在浏览器中输入URL后,浏览器会经历以下关键步骤来渲染页面:
下载HTML文件并解析成DOM树
下载CSS文件并解析成CSSOM树
将DOM和CSSOM合成渲染树(RenderTree)
生成布局树(Layout),计算元素位置和大小
创建图层,处理堆叠上下文
合并图层
光栅化,将图层转换为像素点
这个过程中,回流和重绘是两个重要且常被讨论的性能瓶颈。
推荐大家可以理解一下下面该图的作用。
什么是BFC与文档流
在理解回流重绘前,需要明确文档流的概念:
- BFC(Block Formatting Context):块级格式化上下文,规定了块级元素从上到下排列
- HTML根元素天然形成第一个BFC
- 触发BFC的常见方式:
float、overflow:hidden、display:flex等
页面怎样渲染的
字节面试题
让我们来看下字节的面试题,下面代码会发生几次回流几次重绘。
<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。上述代码理论上会发生四次回流,但是实际只会发生一次回流。浏览器内置了渲染队列,当浏览器执行到一个需要回流的操作之后,会先将该操作放入队列,继续往下执行,如果下面还有导致回流的行为,就一直入队列,直到队列达到阈值或者后面没有新的回流行为,才会将渲染队列中的行为一次性执行,并一次性修改样式。
让我们回到字节的面试题当中,这里大家是不是知道现在发生了一次回流与重绘呀🌹🧡🌹
<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开头的属性都会导致渲染队列重新执行。
让我们再重新回到字节的面试题:只会回流一次的原因,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>
可以看到效果和控制台的打印数据:此时只会发生两次回流,消失的那一刻会发生一次,出现的那一次会发生回流。
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等。