Description
浏览器渲染过程
- 1、解析HTML,构建DOM树
- 2、解析CSS,构建CSSOM树
- 3、将DOM树和CSSOM树合并成一个渲染树
- 4、根据渲染树来进行布局,计算每个节点的几何信息
- 5、将各个节点绘制到屏幕上
为了构建渲染树,浏览器主要完成以下几个工作:
- 1、从DOM树的根节点开始遍历每个可见节点。
- 某些节点不可见(比如script,meta标签等),因为它们不会体现在渲染输出中,所以会忽略。
- 某些节点通过CSS隐藏,因此在渲染树中也会被忽略,比如一个节点设置了“dispaly:none”属性
- 2、对于每个可见节点,为其找到适配的CSSOM规则并应用它们。
- 3、根据每个可见节点以及它们对应的样式,组合生成渲染树。
注意的是:渲染树只包含可见节点
回流
浏览器将DOM树和CSSOM树组合成渲染树之后,还需要计算节点在设备视口内的确切位置和大小,这个计算的阶段就是回流。
为弄清每个节点在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。我们来看一下例子:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html>
我们可以看到第一个div将节点的显示尺寸设置为视口宽度的50%,父div包含的第二个div将其宽度设置为其父节点宽度的50%,即视口的25%。而在回流阶段,我们就需要根据视口宽度,将其转为具体的像素值。
重绘
既然我们知道了哪些可见节点,它们的计算样式以及几何信息,我们终于可以将这些信息传递给最后一个阶段,将渲染树上的每个节点转换成屏幕上的实际像素。这一步通常被称为:重绘。
什么时候会发送回流重绘
我们了解到,回流阶段主要是计算节点的位置或几何信息,那么当页面布局和几何信息发生变化的时候,就需要进行回流。比如:
- 1、添加或删除可见DOM元素
- 2、DOM元素的位置发生变化
- 3、DOM元素的尺寸大小发生变化
- 4、DOM元素的内容发生变化
- 5、浏览器的窗口尺寸发生变化
这里需要注意的是:回流一定会导致重绘,但是重绘不一定会导致回流
当我们获取DOM节点的布局信息时,也会导致浏览器回流重绘。比如:
- offsetTop、offsetLeft、offsetWidth、offsetHeight、width、height等
当我们获取节点的这些属性值时,都会导致浏览器回流重绘,因为当我们获取这些值的时候,需要返回最新的值,所以浏览器会触发回流,重新计算布局信息,来返回正确的值。
减少回流和重绘
1、合并多次对DOM样式的修改
let box = document.getElementById('box');
box.style.padding = '10px';
box.style.borderTop = '20px';
box.style.width = '100px';
向上面的例子中,每次重新设置节点的样式都会导致浏览器回流重绘,所以最好是将修改样式的代码合并成一句,比如:
box.style.cssText = 'padding:10px;borderTop : 20px;width:100px';
或者也可以通过添加一个样式类,来达到这样的效果。
批量修改DOM
当我们需要对DOM进行一系列修改的时候,我们可以这样来减少回流重绘次数:使元素脱离文档流,然后对其进行多次修改,最后将元素放回到文档中。
这里有三种方式可以使DOM脱离文档流:
- 隐藏元素,然后对元素修改,最后再重新显示
- 使用文档片段(document Fragment)
- 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素
如果我们需要通过循环,批量插入节点到文档中,一般的做法都是:
let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];
let appendToList = function (target , data) {
for (let i = 0 ; i < data.length ; i++) {
let li = document.createElement('li');
li.innerHTML = data[i];
target.appendChild(li);
}
};
appendToList(list , data);
但是每一次插入节点的时候都会引起浏览器回流重绘,所以我们可以使用这三种方式来进行优化:
隐藏元素
let list = document.getElementById('list');
list.style.display = 'none';
let data = [1,2,3,4,5,6,7,8,9,10];
let appendToList = function (target , data) {
for (let i = 0 ; i < data.length ; i++) {
let li = document.createElement('li');
li.innerHTML = data[i];
target.appendChild(li);
}
};
appendToList(list , data);
list.style.display = 'block';
使用文档片段
let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];
let appendToList = function (target , data) {
for (let i = 0 ; i < data.length ; i++) {
let li = document.createElement('li');
li.innerHTML = data[i];
target.appendChild(li);
}
};
let fragment = document.createDocumentFragment();
appendToList(fragment , data);
list.appendChild(fragment);
将原始元素拷贝到一个脱离文档的节点中
let list = document.getElementById('list');
let data = [1,2,3,4,5,6,7,8,9,10];
let appendToList = function (target , data) {
for (let i = 0 ; i < data.length ; i++) {
let li = document.createElement('li');
li.innerHTML = data[i];
target.appendChild(li);
}
};
let clone = list.cloneNode(true);
appendToList(clone , data);
list.parentNode.replaceChild(clone , list);
对于一些复杂的动画,可以使用绝对定位使其脱离文档流
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#box {
background: red;
animation: scale 4s linear 0s infinite alternate;
background-image: linear-gradient(to right, rgba( 0,0,0,0.9 ) 25%, rgba( 0,0,0,0.1 ) 50%, rgba( 0,0,0,0.9 ) 75%);
will-change: all;
transform: translate3d(0, 0, 0);
}
@keyframes scale {
from {
width: 100px;
height: 100px;
background: red;
margin: 10px;
transform: rotate(0);
margin-left: -20%;
rotate: 10deg;
}
to {
width: 200px;
height: 200px;
background: yellow;
margin: 50px;
transform: rotate(360deg);
margin-left: 100%;
}
}
</style>
</head>
<body>
<div id="box"></div>
<button id="btn">click</button>
<p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
<p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
<p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
<p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
<p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
<p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p><p>12</p>
<script>
let box = document.getElementById('box');
btn.addEventListener('click' , function () {
box.style.position = 'absolute';
});
let frame = 0;
let start = null;
function loop (timestamp) {
if (!start) {
start = timestamp;
}
frame++;
let diff = timestamp - start;
if (diff > 1000) {
console.log('1秒内刷新频率为:' + Math.round((frame * 1000) / diff));
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop)
</script>
</body>
</html>