参考 - 强烈推荐看看,这个作者写了很多特别好的文章.
浏览器渲染过程
- 解析HTML,生成DOM树; 解析CSS生成CSSOM树
- 将DOM树和CSSOM树合并,生成渲染(Render)树
- Layout(回流): 根据生成的渲染树,视口(viewport),得到节点的几何信息(位置、大小)
- Painting(重绘): 根据渲染树和几何信息得到节点的绝对像素
- Display: 将像素发送给GPU,展示在页面上
生成渲染树
为了构建渲染树,浏览器主要完成了以下工作:
- 从DOM树的根节点开始遍历每个可见节点
- 对于每个可见的节点,找到CSSOM树中的规则,并应用它们
- 根据每个可见节点及其对应的样式,组合生成渲染树
【不可见的节点】:
- 一些不会渲染输出的节点: 比如script、meta、link等
- 一些通过css进行隐藏的节点。比如display: none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上
【注意】: 渲染树只包括可见的节点
回流(Layout)
前面将DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。看下面的栗子:
<!DOCTYPE html>
<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>Cretical Path: Hello Marron!</title></head><body><div style="width: 50%"><div style="width: 50%">Hi Marron, best wish!</div></div></body>
</html>
我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%.而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。
重绘 (Painting)
- 生成渲染树阶段: 我们直到了哪些节点是可见的以及可见节点的样式
- 在回流阶段: 我们得到了可见元素的具体几何信息
我们得到的信息,最终都会托付给GPU进行渲染
GPU的渲染需要具体的像素位置,这就是重绘阶段所做的事情: 根据渲染树和几何信息计算出绝对像素点.
何时发生回流重绘
回流主要是计算节点的几何位置和几何像素大小.那么当页面布局和几何信息发生变化的时候,就需要回流:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(内/外边距、边框大小、高度和宽度等)
- 内容发生: 文本发生变化或图片被另一个不同尺寸的图片所替代
- 页面刚开始渲染的时候
- 浏览器的窗口尺寸变化: 回流是根据视口的大小来计算元素的位置和大小的
经典老话: 回流一定重绘,重绘不一定回流
浏览器的优化机制
现代的浏览器都是很聪明的,由于每次重排都会造成造成额外的计算消耗,因此大多数浏览器都会通过队列修改、批量执行来优化重排过程。浏览器会将修改操作放在队列里,直到过了一段时间,或者操作达到一个阈值,才清空队列。
还有一些强制刷新的属性(避免使用):
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
- getBoundingClientRect
- …
前端优化
1 -【并多次的DOM和添加样式】
// 未优化前 - 3次
const el = document.getElementById('test')
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';// 合并样式 - 1次
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px'// 添加样式 - 1次
const el = document.getElementById('test');
el.calssName += ' active';
2 -【脱离文档流】
当元素脱离文档流后,对元素的所有操作都不会引起回流和重绘.因此如果,对某个元素进行的DOM操作比较多的时候,可以先将元素脱离文档流,然后操作,最后在放回文档流。具体操作如下:
- 使元素脱离文档流
- 对其进行多次修改
- 将元素带回到文档中.
[注] : 上述的1、3会引起回流和重绘.
【脱离文档流的方法】
- 隐藏元素,修改应用,重新显示
- 使用文档片段(document fragment)在使用DOM之外构建一个子树,再把它拷贝回文档
- 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
// 每次插入li都会引起一次回流和重绘
function appendDataToElement(appendToElement, data) {let li;for(let i =0,len = data.length;i < len;i++){li = document.createElement('li');li.textContent = 'text';appendToElement.appendChild(li);}
}const ul = document.getElementById('list');
appdenDataToElement(ul, data);
[隐藏元素]
// 仅在隐藏元素和现实元素时产生2次回流和重绘
function appendDataToElement(appendToElement, data) {let li;for(let i =0, len = data.length; i < len; i++){li = document.createElement('li');li.textContent = 'text';appendTOElement.appendChild(li);}
}const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
[使用文档片段] - 在当前DOM外构建一个子树,再把它拷贝回文档
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
up.appendChild(fragment);
[脱离文档] - 将原始元素拷贝到一个脱离文档的节点中,修改节点,再替换原始的元素。
const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);
[注] - 现代浏览器使用了队列来存储多次修改,因此上述的优化可能效果不是很理想.
3 - 【避免触发同步布局事件】
// 栗子: 多次使用到 offsetWidth 属性
function initP(){for(let i = 0; i< paragraph.length; i++){paragraph[i].style.width = box.offsetWidth + 'px'}
}
上述代码每次循环,都会使浏览器强制刷新队列(box.offsetWidth
),造成多次回流和重绘.改进如下:
const width = box.offsetWidth;
function initP(){for(let i = 0; i < paragraph.length; i++){paragraph[i].style.width = width + 'px'}
}
4 - 【复杂动画的优化】
对于复杂动画效果,由于会经常的引起回流和重绘。因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流 - 栗子