浏览器的渲染过程
从上面这个图上,我们可以看到,浏览器渲染过程如下
解析 HTML 生成 DOM 树,解析 CSS 生成 CSSOM 树
将 DOM 树和 CSSOM 树结合生成渲染树 renderTree
Layout(回流): 根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素
Display: 将像素发送给 GPU,展示在页面上。
生成渲染树(RenderTree)
为了构建渲染树,浏览器主要完成了以下工作
从 DOM 树的根节点开始遍历每个可见节点。
对于每个可见的节点,找到 CSSOM 树中对应的规则,并应用它们。
根据每个可见节点以及其对应的样式,组合生成渲染树。
第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:
一些不会渲染输出的节点,比如 script、meta、link 等。
一些通过 css 进行隐藏的节点。比如 display:none。注意,利用 visibility 和 opacity 隐藏的节点,还是会显示在渲染树上的。只有 display:none 的节点才不会显示在渲染树上。
回流(Layout)
前面我们通过构造渲染树,我们将可见 DOM 节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口 (viewport) 内的确切位置和大小,这个计算的阶段就是回流。为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历, 而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值
重绘(Painting)
通过回流 (Layout) 阶段,我们知道了所有的可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。
何时发生回流重绘
回流阶段是计算节点的几何信息和位置,那么当页面布局或者几何信息发生改变时,就需要回流。
添加或者删除可见的 DOM 元素
元素的位置、尺寸发生变化
页面开始渲染的时候(这肯定避免不了)
浏览器的视口尺寸大小发生改变(因为回流是根据浏览器视口的大小来计算元素的位置和尺寸大小)
注意:回流一定会触发重绘,而重绘不一定会回流
根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点。
基于回流 (Layout)、重绘(Painting) 的优化方法
避免扰乱现代浏览器的优化机制
在现代浏览器的中,由于每次回流、重绘的时候,都需要额外的计算消耗,因此会通过队列化修改,并批量执行来优化这一过程。浏览器会将修改操作放入队列里面,直到过了一段时间或者达到一个阈值,才清空队列。
但是当你获取布局信息时,会强制刷新队列,例如:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
getBoundingClientRect()
上面这些方法,都需要获取最新的布局信息,所以浏览器会强制刷新队列并执行回流、重绘,来获取最新的信息。因此我们在修改样式的时候,应该尽量避免使用上面的属性、方法,如果非要使用,可以先缓存起来然后一起获取。
CSS 的修改方式
考虑以下代码
const el = document.getElementById(‘el’)
el.style.padding = ‘xxx’
el.style.margin = ‘xxx’
el.style.border = ‘xxx’
这里元素的几何信息有三次被修改了,但是现代浏览器会将起缓存起来,但是如果这期间有通过前面列出来的属性、方法访问位置信息的话就会触发三次回流、重绘。所以还是建议通过 cssText 或者 class 的方法一次性修改。
el.style.cssText += ‘border-left: 1px; border-right: 2px; padding: 5px;’;
// 或者
el.className += ‘xxx’;
批量修改 DOM
当我们需要对 DOM 进行一系列修改的时候,可以通过以下几种方式减少回流重绘次数:
隐藏元素,应用修改,重新显示
function appendDataToElement (appendToElement, data) {
let li;
for (let i = 0; i < data.length; 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’; // 操作完以后再可见
使用文档片段 (document fragment) 在当前 DOM 之外构建一个子树,再把它拷贝回文档。
const ul = document.getElementById(‘list’);
const fragment = document.createDocumentFragment()
appendDataToElement(fragment , data);
ul.appendChild(fragment)
对于复杂动画效果, 使用绝对定位让其脱离文档流
对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。
CSS3 硬件加速