**回流必定触发重绘, 重绘不一定触发回流,**重绘的开销较小,回流的代价较高。
一、回流(Reflow)
回流:也叫重排,是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。
引起重排的因素:
-
页面初次渲染
-
浏览器窗口大小改变
-
改变 DOM 元素的几何属性
如:常见的几何属性有 width、height、padding、margin、left、top、border。元素尺寸、位置、内容发生改变。元素字体大小变化(font-size)。
- 改变 DOM 树的结构
添加或者删除可见的 dom 元素。浏览器引擎布局的过程,顺序上可以类比于树的前序遍历——它是一个从上到下、从左到右的过程。通常在这个过程中,当前元素不会再影响其前面已经遍历过的元素。
- 获取一些特定属性的值
以下这些值有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流。
属性:clientWidth、clientHeight、clientTop、clientLeft、offsetWidth、offsetHeight、offsetTop、offsetLeft、scrollWidth、scrollHeight、scrollTop、scrollLeft、getComputedStyle()、getBoundingClientRect()、scrollTo()。
- 激活 CSS 伪类(例如::hover)
二、重绘(Repaint)
重绘:是Dom元素的样式改变,但是不影响布局(比如修改了颜色或背景色),此时浏览器只需要对ui层面的像素绘制,因此,损耗小。
例如outline,visibility,color、background-color等。
三、浏览器的渲染过程:
1、解析HTML,生成DOM树, 解析css生成CSSOM树。
2、将DOM树与CSSOM树结合,生成渲染树(Render Tree):渲染树只包含可见的节点
3、回流:根据生成的渲染树,进行回流,得到元素的几何信息(位置,大小)。
4、重绘:根据渲染树以及回流得到的几何信息,得到节点的绝对像素。
5、Display:将像素发送给GPU,展示在页面上。
构建渲染树,浏览器主要完成了以下工作:
- 从DOM树的根节点开始遍历每个可见节点。
- 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
- 根据每个可见节点以及其对应的样式,组合生成渲染树。
不可见节点包括:
- 一些不会渲染输出的节点,比如script、meta、link等。
- 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。
四、减少回流和重绘
1、批量修改DOM
当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:
- 使元素脱离文档流
- 对其进行多次修改
- 将元素带回到文档中。
第一次与第三次都会导致回流。
元素脱离文档流的方式:
- 隐藏元素,应用修改,重新显示
- 使用文档片段(document fragment)在当前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');
appendDataToElement(ul, data);
修改后:
第一种:先隐藏元素,然后批量应用修改,最后重新显示。
这个会在展示和隐藏节点的时候,产生两次重绘
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);
**2、合并多次对DOM和样式的修改,**避免逐条改变样式,使用类名去合并样式。
例:
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
改为:修改CSS的class
// ---css
.basic_style {
width: 100px;
height: 200px;
border: 10px solid red;
color: red;
}
//---js
const container = document.getElementById('container');
container.classList.add('basic_style');
3、避免触发同步布局事件
也就是获取某些属性会导致页面回流,当在for循环中重复获取时,就会导致多次重绘,我们可以减少这种不必要的操作,如:
当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px'; //每一次for循环都会触发回流
}
}
可以优化为:
const width = box.offsetWidth; //只触发一次回流
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}