前端性能优化之减少重排和重绘

首先需要了解一下浏览器渲染HTML的大致过程:

(1)HTML被HTML解析器解析成DOM Tree, css则被css解析器解析成CSSOM Tree(并行解析)。

(2)DOM Tree和CSSOM Tree解析完成后,被合并到一起,形成渲染树(render Tree)。

(3)重排:节点信息计算,即根据渲染树计算每个节点的几何信息(大小及位置)。

(4)重绘:渲染绘制,即根据计算好的信息绘制整个页面,渲染出最终的页面。

重点: 以上4步简述浏览器的一次渲染过程,理论上,每一次的dom更改或者css几何属性更改,都会引起一次浏览器的重排和重绘过程,而如果是css的非几何属性更改,则只会引起重绘过程。所以重排一定会引起重绘,而重绘不一定会引起重排。

一、重排(reflow)

某个子元素样式发生改变,直接影响到了其父元素以及往上追溯很多祖先元素(包括兄弟元素),这个时候浏览器要重新去渲染这个子元素相关联的所有元素的过程就称为重排(也称回流)。

注: 重排几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。

下面情况会导致重排:

(1)页面首次渲染(所以渲染页面至少会有一次重排和重绘)
(2)改变字体大小或数量
(3)元素内容变化(文字数量或图片大小等等)
(4)激活伪类,如:hover
(5)操作class属性
(6)添加或者删除可见的DOM元素。
(7)查询某些属性或调用某些方法(查询offsetWidth或offsetHeight等)
(8)设置style属性
(9)改变浏览器窗口大小。

记忆:主要是元素大小和位置的改变以及查询某些属性

二、重绘(repain)

相比重排,重绘就简单多了。所谓重绘,就是当页面中元素外观样式的改变并不影响它在文档流中的位置。例如只是改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性,将只会引起浏览器重绘。所以重绘的速度明显快于重排。

三、为什么要减少重排和重绘?

(1)整个在浏览器的渲染过程中(页面初始化,用户行为改变界面样式,动画改变界面样式等)重排和重绘会大大影响web性能。
(2)重绘和重排的开销是非常昂贵的,如果我们不停的在改变页面的布局,就会造成浏览器耗费大量的开销在进行页面的计算,这样的话,我们页面在用户使用起来,就会出现明显的卡顿。

四、减少重排和重绘的方法

(1)减少使用以下几何属性

offsetTop, offsetLeft,...
scrollTop, scrollLeft, ...
clientTop, clientLeft, ...

原理:
大多数浏览器其实自带优化: 通过队列化修改并批量执行来优化重排过程。即先将导致重排和重绘的操作先压入到一个队列中,达到一定数量后再一起处理,达到减少重排和重绘的目的。而这些属性,都是需要实时回馈给用户的几何属性或者是布局属性,当然不能再依靠浏览器的优化,因此浏览器不得不立即执行渲染队列中的“待处理变化”,触发重排返回正确的值。

(2)减少DOM操作

var el = document.querySelector('.el');
el.style.borderLeft = '1px'; //一次重排
el.style.borderRight = '2px'; //两次重排
el.style.padding = '5px'; //三次重排
//操作了三次dom

上面操作会导致三次重排,如何优化?

// css 
.active {
    padding: 5px;
    border-left: 1px;
    border-right: 2px;
}
// javascript
var el = document.querySelector('.el');
el.className = 'active'; //只操作一次dom

直接更换类名,只触发了一次重排。
结论: 修改元素的多个样式时,直接更换类名,减少重排。(或使用使用cssText属性,不太熟悉,就不做介绍了)

(3)批量修改DOM元素

核心思想:先让该元素脱离文档流,对其进行多重改变,再将元素带回文档中。
关键:元素脱离文档流之后对其进行操作不会导致重排。下面举几个栗子。

1)比如我们要给ul添加多个li时:

let ul = document.querySelector('#mylist');
ul.style.display = 'none';
appendNode(ul, li);
appendNode(ul, li);
appendNode(ul, li);
...........
ul.style.display = 'block';

原理:display设置为none时元素脱离文档流,不会出现在render tree 中。最终只造成两次重排,分别发生在控制元素的隐藏和显示时。

2)使用文档片段创建一个子树,然后再拷贝到文档中

let fragment = document.createDocumentFragment();
appendNode(fragment, li);
appendNode(fragment, li);
appendNode(fragment, li);
..........
ul.appendChild(fragment);

原理: DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面重排。
结果: 只触发了一次重排,插入文档片段时。

3)将原始元素拷贝到一个独立的节点中,操作这个节点,然后覆盖原始元素

let old = document.querySelector('#mylist');
let clone = old.cloneNode(true);
appendNode(clone, li);
old.parentNode.replaceChild(clone, old);

结果: 只触发了一次重排

其他一些方案(不做详细介绍):

(1)开启动画的GPU加速,把渲染计算交给GPU。
(2)用事件委托来减少事件处理器的数量。
(3)实现元素的动画,它的position属性,最好是设为absoulte或fixed,这样不会影响其他元素的布局
(4)不要使用table布局,因为table中某个元素旦触发了reflow,那么整个table的元素都会触发reflow。
(5)少用css表达式

参考文章:
细说浏览器渲染的重排与重绘
前端性能优化之重排和重绘

你可能感兴趣的:(前端性能优化,css3,css,javascript,html,html5)