减少DOM的回流和重绘

5,496 阅读6分钟

一、回流 Reflow & 重绘 Repaint

1、回流 Reflow

元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候),触发了重新布局,导致渲染树重新计算布局和渲染

  • 如添加或删除可见的DOM元素;
  • 元素的位置发生变化;
  • 元素的尺寸发生变化;
  • 内容发生变化(比如文本变化或图片被另一个不同尺寸的图片所替代);
  • 页面一开始渲染的时候(这个无法避免);
  • 因为回流是根据视口的大小来计算元素的位置和大小的,所以浏览器的窗口尺寸变化也会引发回流......

第一次渲染完成后,基于JS改变了 元素的位置或者大小,浏览器需要重新计算渲染树中 每一个元素在视口中的 位置和大小
=> 这个阶段就是=> 重新布局 / 回流 / 重排 / reflow

2、重绘 Repaint

元素样式的改变(但宽高、大小、位置等不变)

  • 如:outline, visibility, color, background-color......等

重绘不是很消耗性能

回流很消耗性能(DOM元素的大小和位置信息都要重新计算一遍),而且一旦发生回流,重新计算完后,还需要重绘

  • 回流一定会触发重绘,而重绘不一定会回流

所以我们项目中要尽量减少操作DOM,在不可避免操作DOM时,我们也要尽量减少回流和重绘,来完成我们的性能优化;

  • 第一次渲染页面的时候:触发一次回流和重绘(不可避免的)

二、避免DOM的回流

1、放弃传统操作 DOM 的时代,基于 vue/react 开始数据影响视图模式

  • mvvm / mvc / virtual dom / dom diff ......

vue / react 数据驱动思想 :

我们自己不操作DOM,我们只操作数据,让框架帮我们根据数据渲染视图(框架内部本身对于DOM的回流和重绘以及其它性能优化做的非常好)

2、分离读写操作(现代浏览器的渲染队列的机制)(重要)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        .box {
            width: 100px;
            height: 100px;
            background: red;
        }
	</style>
</head>
<body>
    <div class="box" id="box"></div>
    <ul id="item">
        <!-- <li>我是第1个LI</li> -->
    </ul>
</body>
</html>
  • 在老版本的浏览器中,我们分别改变了三次样式(都涉及了位置或者大小的改变),所以触发三次回流和重绘
// SCRIPT在DOM结构末尾导入,可以直接使用元素的ID代表这个元素对象
box.style.width = '200px';
box.style.height = '200px';
box.style.margin = '20px'; 
//=> 在老版本的浏览器中,我们分别改变了三次样式(都涉及了位置或者大小的改变),所以触发三次回流和重绘
  • 现代浏览器中默认增加了“渲染队列的机制”,以此来减少DOM的回流和重绘

=> 遇到一行修改样式的代码,先放到渲染队列中,继续看 下面一行代码 是否还为修改样式的,如果是继续增加到渲染队列中...直到下面的代码不再是修改样式的,而是获取样式的代码!此时不再向渲染队列中增加,把之前渲染队列中要修改的样式一次性渲染到页面中,引发一次DOM的回流和重绘

// 一次回流重绘
box.style.width = '200px';
box.style.height = '200px';
box.style.margin = '20px'; 

所以我们利用这种机制来减少DOM的回流

// 三次回流和重绘
box.style.width = '200px';
console.log(box.style.width); //=>中断渲染队列,立即渲染一次,引发一次DOM回流和重绘  200px
box.style.height = '200px'; 
console.log(box.offsetHeight);
box.style.margin = '20px'; 

// 一次回流重绘
box.style.width = '200px';
box.style.height = '200px';
box.style.margin = '20px';
console.log(box.style.width);
console.log(box.offsetHeight); 

但是有的时候我们还需要浏览器多次回流:例如:先设置动画,渲染后,在去改变样式,让其有动画效果

//=> 此时动画并没有体现
box.style.transition = '.3s';
box.style.width = '200px';
box.style.height = '200px'; 

// 手动不分离读写的需求:先设置动画,渲染后,在去改变样式,让其有动画效果
box.style.transition = '.3s';
let AA = box.offsetHeight;
box.style.width = '200px';
box.style.height = '200px'; 

3、样式集中改变(不重要)

  • 通过修改样式类:把样式实现写好,我们后期通过样式类修改样式
  • 用cssText的方式添加想要修改的样式
//=> 通过修改样式类:把样式实现写好,我们后期通过样式类修改样式
// 一次回流重绘
box.className = 'active';

//=> 把所有想写的样式,用cssText的方式添加
// 一次回流重绘
box.style.cssText = 'width:200px;height:200px;';

4、缓存布局信息(不重要)

把要操作的内容一次都拿到,然后用变量存储,想设置的时候直接拿变量值即可,不用在重新获取了,和分离读写的原理类似

div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
=> 改为
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';

5、元素批量修改(重要)

DOM的增加也会引起回流重绘

/* 在动态操作DOM结构中的优化(例如:数据绑定) */

for (let i = 1; i <= 5; i++) {
    let liBox = document.createElement('li');
    liBox.innerText = `我是第${i}个LI`;
    item.appendChild(liBox); 
    //=>每一次向页面中增加,都会触发一次DOM的回流和重绘(5次)
} 
  • 文档碎片:临时创建的一个存放文档的容器,我们可以把新创建的LI,存放到容器中,当所有的LI都存储完,我们统一把容器中的内容增加到页面中(只触发一次回流)
// 文档碎片:临时创建的一个存放文档的容器,我们可以把新创建的LI,存放到容器中,当所有的LI都存储完,我们统一把容器中的内容增加到页面中(只触发一次回流)
let frag = document.createDocumentFragment();
for (let i = 1; i <= 5; i++) {
    let liBox = document.createElement('li');
    liBox.innerText = `我是第${i}个LI`;
    frag.appendChild(liBox);
}
item.appendChild(frag); 
  • 字符串拼接:项目中,有一个文档碎片类似的方式,也是把要创建的LI事先存储好,最后统一放到页面中渲染(字符串拼接)
// 项目中,有一个文档碎片类似的方式,也是把要创建的LI事先存储好,最后统一放到页面中渲染(字符串拼接)
let str = ``;
for (let i = 1; i <= 5; i++) {
    str += `<li>我是第${i}个LI</li>`;
}
item.innerHTML = str; 

6、动画效果应用到 position 属性为 absolute 或 fixed 的元素上(脱离文档流)

  • 也会引起回流重绘,只不过从新计算过程中,因为他脱离文档流了,不会对其他元素产生影响,重新计算的过程中比较快一点

7、CSS硬件加速(GPU加速)

  • 比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘:transform/opacity/filters......这些属性会触发硬件加速,不会引发回流和重绘......
  • 可能会引发坑:过多使用会占用大量内存,性能消耗严重,有时候会导致字体模糊等

8、牺牲平滑度换取速度

  • 每次1像素移动一个动画,但是如果此动画使用了100%的CPU,动画就会看上去是跳动的,因为浏览器正在与回流做东征。每次移动3像素可能开起来平滑度低了,但它不会导致CPU在较慢的机器中抖动

9、避免 table 布局和使用 css 的javascript表达式

思维导图