CSS回流与重绘

210 阅读4分钟

**回流必定触发重绘, 重绘不一定触发回流,**重绘的开销较小,回流的代价较高。

一、回流(Reflow)

回流:也叫重排是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。

引起重排的因素:

  • 页面初次渲染

  • 浏览器窗口大小改变

  • 改变 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,colorbackground-color等。

三、浏览器的渲染过程:

1、解析HTML,生成DOM树, 解析css生成CSSOM树。

2、将DOM树与CSSOM树结合,生成渲染树(Render Tree):渲染树只包含可见的节点

3、回流:根据生成的渲染树,进行回流,得到元素的几何信息(位置,大小)。

4、重绘:根据渲染树以及回流得到的几何信息,得到节点的绝对像素。

5、Display:将像素发送给GPU,展示在页面上。

构建渲染树,浏览器主要完成了以下工作:

  1. 从DOM树的根节点开始遍历每个可见节点
  2. 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
  3. 根据每个可见节点以及其对应的样式,组合生成渲染树。

不可见节点包括:

  • 一些不会渲染输出的节点,比如script、meta、link等。
  • 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。

四、减少回流和重绘

1、批量修改DOM

当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:

  1. 使元素脱离文档流
  2. 对其进行多次修改
  3. 将元素带回到文档中。

第一次与第三次都会导致回流。

元素脱离文档流的方式:

  • 隐藏元素,应用修改,重新显示
  • 使用文档片段(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';
    }
}