前端知识查漏补缺(1):详解回流和重绘及优化

452 阅读4分钟

先了解一下浏览器的渲染机制

以上两张图来自MDN,我们可以看出,浏览器的渲染工作过程如下:

  1. 浏览器解析HTML,生成DOM树;解析CSS,生成CSSOM树;
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree);
  3. Layout阶段:根据生成的渲染树,进行Layout,得到节点的几何信息(位置,大小);
  4. Painting阶段:根据渲染树以及回流得到的几何信息,得到节点的绝对像素;
  5. Display:将像素发送给GPU,展示在页面上。

我们记住浏览器渲染页面的这五个主要流程,下面会用到。

回流or重排 (Reflow)

元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候),触发了重新布局,导致渲染树重新计算布局和渲染,会把上面浏览器渲染1-5流程重新走一遍,这个过程称为:回流(Reflow)

由此也得到,频繁触发浏览器的回流,对性能损耗较大。

因为现在的浏览器都是使用流式布局模型 (Flow Based Layout),一旦文档流中某个元素发生形状改变,势必牵一发而动全身。

会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)
  • 查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

clientWidth、clientHeight、clientTop、clientLeft offsetWidth、offsetHeight、offsetTop、offsetLeft scrollWidth、scrollHeight、scrollTop、scrollLeft scrollIntoView()、scrollIntoViewIfNeeded() getComputedStyle() getBoundingClientRect() scrollTo()

重绘 (Repaint)

元素样式的改变(但宽高、大小、位置等不变),浏览器根据新样式属性进行对应更新,这个过程叫重绘(Repaint)

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

对比回流,重绘对浏览器性能消耗较小。

这里也能明白,回流一定重绘,重绘不一定回流。

减少DOM的回流和重绘

1、感谢现代浏览器的渲染队列机制

可以先来看下这样一道题,问触发几次回流重绘:

来自于掘友:阳光是sunny

答案如掘金用户阳光是sunny所说:古董浏览器需要两次,现代浏览器只要一次即可。

现代浏览器中默认增加了“渲染队列的机制”,以此来减少DOM的回流和重绘。其工作原理如下:

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

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

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

3、批量修改DOM

比如,像页面中的ul元素中动态添加li:

for (let i = 1; i <= 100; i++) {
    let liBox = document.createElement('li');
    liBox.innerText = `我是第${i}个LI`;
    ul.appendChild(liBox); 
    //=>每一次向页面中增加,都会触发一次DOM的回流和重绘(100次)
} 

为了减少不必要的回流和重绘,可以采取以下方式:

  • 字符串拼接方式 :先把要更新的DOM拼接成字符串,最后一次性统一渲染。
let str = ``;
for (let i = 1; i <= 100; i++) {
    str += `<li>我是第${i}个LI</li>`;
}
ul.innerHTML = str; 
  • 文档碎片方式:临时创建的一个存放文档的容器,我们可以把新创建的元素,存放到容器中,当所有的元素都存储完,我们统一把容器中的内容增加到页面中(只触发一次回流)
let frag = document.createDocumentFragment();
for (let i = 1; i <= 100; i++) {
    let liBox = document.createElement('li');
    liBox.innerText = `我是第${i}个LI`;
    frag.appendChild(liBox);
}
ul.appendChild(frag); 

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

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

5、CSS方面补充

  • 避免使用table布局;
  • 避免使用CSS表达式(例如:calc())
  • CSS硬件加速(GPU加速)

参考文章