搜到的网页中感觉比较简洁清楚的是frendyguo.me/excessive-r… 我这篇文章大部分内容可能就是把它翻译一下,但中间也会穿插一些我的理解。
浏览器的渲染步骤
Web browser took a number of steps also known as CRP (Critical Rendering Path) in order to paint elements on your screen. Essentially there are four main steps: Parsing, Render Tree, Layout, and Paint.
这段话的要点是说:浏览器是通过被称为CRP (Critical Rendering Path)来把网页绘制到屏幕上的,而CRP基本上是4个步骤。Parsing, Render Tree, Layout, and Paint。对照上面的图也就是:
- 把html parse成dom树,把css parse成cssom树,
- 把两者叠加为render tree;
- 计算元素的位置和dimension(例如宽度)
- 将元素转化为实际的像素,绘制到屏幕上。
例子:
假设有下面的代码
<html>
<head>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<p>
Hello <span>World</span>
</p>
<div>How are you?</div>
</body>
</html>
p {
font-size: 12px;
font-weight: 400;
}
span {
color: red;
}
div {
color: red;
}
- 浏览器拿到文件时,首先会进行parse。
html会被解析为类似于下面这样的结构。在解析过程中,如果结点有外部资源的link,那么当link中的资源被解析后parse才会继续执行。
而css则是下面这样的结构:
2. 当上面的DOM和 CSSOM 都构建完成后,它们会被组合在一起,形成render tree。
display: none;的元素会被忽略。不过
3. layout:
在这个阶段,浏览器会遍历render tree,然后进行计算以获取每个元素的position和dimension信息(dimension应该指的是例如宽度这样的概念吧)。结点数据越多,这个过程花费的时间就越长。
- paint 这个步骤就是把每个像素渲染到屏幕上。
其实reflow对应的就是layout阶段,只不过第一次进行这个阶段时它被称为layout,后续它就会被称为reflow。
同样,repaint也是对应paint,第一次被称为paint,之后被称为repaint
reflow
它主要要做的就是:
a web browser recalculates the position and dimension of elements
上面也提过,其实就是Critical Rendering Path中的layout阶段。
reflow的性能消耗比较高,但是它却很容易被触发。当触发时,不仅会影响触发元素本身,还会影响它的children、ancestors、以及它后面的dom。
它的触发情况基本可以被归为以下两大类:
- dom出现了会影响layout的调整。例如:调整了div的宽度,插入了一个新元素。一个元素本来是display:none的,却改为了block。
- 在mutation发生过后获取measurement。 例如:offsetHeight。浏览器其实会把每个结点的信息缓存起来,但如果页面已发生了改变,那么之前的缓存就失效了,浏览器需要重新计算。
根据这篇文章, 这里可以补充一个信息是: reflow比较昂贵,所以当有reflow的需求时,浏览器可能不会马上执行,而是会放到内部的等待队列中,而是到下一个frame执行。(window.requestAnimationFrame())所以,当获取元素的物理属性,例如width时,有可能等待队列还有有样式修改在等着,所以为了保证获取的物理属性是对的,浏览器会强制执行一次reflow。
用Chrome DevTools查看reflow次数
Chrome DevTools中的performance可以让我们看到layout以及paint的次数等信息。
以下面代码为例:
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
height: 100px;
width: 100px;
background-color: pink;
}
</style>
</head>
<body>
<html>
<body>
<div id="box1"></div>
<div id="box2"></div>
<div id="box3"></div>
<div id="box4"></div>
<div id="box5"></div>
<div id="box6"></div>
</body>
</html>
<script>
var box1Height = document.getElementById('box1').clientHeight; // 第一次layout forced
document.getElementById('box1').style.height = box1Height + 10 + 'px';
var box2Height = document.getElementById('box2').clientHeight; // 第二次layout forced
document.getElementById('box2').style.height = box2Height + 10 + 'px';
var box3Height = document.getElementById('box3').clientHeight; //
document.getElementById('box3').style.height = box3Height + 10 + 'px';
var box4Height = document.getElementById('box4').clientHeight; //
document.getElementById('box4').style.height = box4Height + 10 + 'px';
var box5Height = document.getElementById('box5').clientHeight; //
document.getElementById('box5').style.height = box5Height + 10 + 'px';
var box6Height = document.getElementById('box6').clientHeight; //
document.getElementById('box6').style.height = box6Height + 10 + 'px';
</script>
</body>
</html>
这个例子是从这篇文章拿来的,但是原文章里一笔带过了,所以我自己尝试分析一下,不保证争取。
原文章里说以上代码会执行6次layout,但我自己尝试用录制performance, 发现似乎执行了7次。
我在上面把7次layout行数后面都标记了//。 根据开发者工具的提示信息和我自己根据现有知识的分析,它可能是这样的思路。
<script>
//这行因为要获取元素物理信息,所以force layout,以确保获取的是最新的信息
var box1Height = document.getElementById('box1').clientHeight;//1
// 这行似乎不会马上执行layout,而是会先进入到队列中
document.getElementById('box1').style.height = box1Height + 10 + 'px';
// 这行跟第一行一样,执行了force layout,这使得第二行和第三行会被合并执行一次layout
var box2Height = document.getElementById('box2').clientHeight;//2
document.getElementById('box2').style.height = box2Height + 10 + 'px';
var box3Height = document.getElementById('box3').clientHeight; // 3
document.getElementById('box3').style.height = box3Height + 10 + 'px';
var box4Height = document.getElementById('box4').clientHeight; // 4
document.getElementById('box4').style.height = box4Height + 10 + 'px';
var box5Height = document.getElementById('box5').clientHeight; // 5
document.getElementById('box5').style.height = box5Height + 10 + 'px';
var box6Height = document.getElementById('box6').clientHeight; // 6
// 这行因为是最后一行了,下面也没有其他要执行的,所以它自己直接执行一次layout。
document.getElementById('box6').style.height = box6Height + 10 + 'px';// 7
</script>
接着我又试了下,把以上脚本改为click触发,再录制一次performance试试看。此时似乎变成了6次,上面的第一次不见了。所以是否第一行的获取宽度是可以和第一次渲染一起执行的? 但是感觉这样不太符合逻辑..这个只能先存疑了,应该是我哪里理解得不对。
换成下面这部分代码,以及用click处理后,layout变成了只有一次
var box1Height = document.getElementById('box1').clientHeight;
var box2Height = document.getElementById('box2').clientHeight;
var box3Height = document.getElementById('box3').clientHeight;
var box4Height = document.getElementById('box4').clientHeight;
var box5Height = document.getElementById('box5').clientHeight;
var box6Height = document.getElementById('box6').clientHeight;
document.getElementById('box1').style.height = box1Height + 10 + 'px'; // 我就是被layout提到的第80行。
document.getElementById('box2').style.height = box2Height + 10 + 'px';
document.getElementById('box3').style.height = box3Height + 10 + 'px';
document.getElementById('box4').style.height = box4Height + 10 + 'px';
document.getElementById('box5').style.height = box5Height + 10 + 'px';
document.getElementById('box6').style.height = box6Height + 10 + 'px';
这里layout信息中提到了第80行,我在上面代码中注释出来了。我猜测这里应该指的是第80行触发了layout,也就是说前面的获取高度可能都会被储存在队列中,毕竟假设我们只获取宽度什么都不做的话,那么浏览器会觉得“我也不要辛辛苦苦去layout获取一个你并不会使用的高度”。然后直到下面一段,浏览器发觉高度还是需要的,采取执行。但我好奇的是:后面的设高度的几行为什么只需要执行一次layout,是浏览器具有这种智能合并的行为吗?
一个例子
dom manipulation
<html>
<head>
<script type="text/javascript">
function addElement() {
const el = document.createElement('div');
el.innerHTML = 'Appended!';
const hello = document.querySelector('#hello');
document.body.insertBefore(el, hello); // Insert before the div will cause layout shift
}
</script>
</head>
<body>
<button onclick="addElement()">Add an element</button>
<div id="hello">Hello World</div>
</body>
</html>
上面这段代码,每点击一次按钮,会触发一次reflow。如果用DocumentFragment,则会大大减少reflow的次数。
<html>
<body>
<div id="hello">Hello World</div>
<script type="text/javascript">
setTimeout(function() {
var fragment = new DocumentFragment();
for (var i=0; i<20; i++) {
const el = document.createElement('div');
el.innerHTML = 'Appended!';
fragment.appendChild(el);
}
document.body.prepend(fragment); // 只有这行会触发一次reflow
}, 1000);
</script>
</body>
</html>