DOM操作的性能优化:深入理解重排和重绘

323 阅读4分钟

前言

大家好,我是抹茶。
对于每个前端来说,重绘和重排都不是什么陌生的词汇,但是当我真的去专门研究前端性能优化的时候,会引起重绘和重排的行为远远超过我认知的那些,所以本文就将系统而具体的归纳总结会引起重绘和重排的行为。

一、重排和重绘的影响

重排是指重新计算布局,排列DOM,也就是从渲染流水线的布局树开始,重新走布局树=>图层树=>绘制=>合成的流程。

image.png

重绘是指重新绘画,也就是从渲染流水线的绘制开始,重新走绘制=>合成的流程。

image.png

二、引起重排和重绘的行为

1.对DOM元素的几何属性的修改。

这些属性包括width、height、padding、margin、left、top等,某元素的这些属性发生变化时,便会波及与它相关的所有节点元素进行几何属性的重新计算,这会导致巨大的计算量。

2.更改DOM树的结构

浏览器进行页面布局时,遵循从上到下,从左到右的顺序。这里对DOM树节点的增、删、移动等操作,只会影响当前节点后的所有节点元素,而不会再次影响已经遍历过的元素。

3.获取某些需要实时计算的属性值

比如页面可见区域宽高offsetWidth、offsetHeight,页面视窗中元素与视窗边界的距离offsetTop、offsetLeft,类似的属性还有scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientWidth、clientHeight以及调用widow.getComputedStyle方法。

三、避免重排和重绘,提升性能的方式

1.使用类名取代对样式逐条修改

在JS中,用代码对元素样式进行逐条修改,是一种糟糕的编码方式。错误代码示范如下

// 获取DOM元素朱行修改样式
const div = document.getElementById('myDiv');
div.style.height = '400px';
div.style.width = '400px';
div.style.border = '1px solid #ddd';

上述代码对样式进行逐行修改,每行都会触发一次对渲染树的更改,于是会导致页面布局重新计算而带来巨大的性能开销。

合适的方案是,将样式写到一个css类选择器中,仅在JS代码中添加或者更改类名。

.my-div{
    height:400px;
    width:400px;
    border1px solid #ddd;
}

在JS中通过给指定的元素添加类的方式,一次完成样式更改,可以避免触发多次对页面布局的重新计算。

2.缓存对敏感属性值的计算

我们分析一下下面的代码

const list = document.getElementById('list');
for(let i = 0; i < 10; i++){
    list.style.top = `${list.offsetTop + 10}px`;
    list.style.left = `${list.offsetLeft + 10}px`;
}

这里获取offsetTop和offsetLeft都会重新触发页面布局的重新计算,赋值的环节也会触发页面布局的计算,导致糟糕的性能。

优化方式是将敏感的属性值通过变量的形式缓存起来,等到计算完成后再统一赋值触发布局重排。

const list = document.getElementById('list');

// 将敏感属性缓存起来
let offsetTop = list.offsetTop,offerLeft = list.offsetLeft;

for(let i = 0; i < 10; i++){
    offsetTop +=10;
    offsetLeft +=10;
}

// 计算完成后统一赋值触发重排
list.style.left = offsetLeft;
list.style.top = offsetTop;

3.用局部变量缩短作用域链的查找

如果一个非局部变量在函数中的使用次数不止一次,那么最好使用局部变量进行存储。举个🌰

function test(){
    const target = document.getElementById('target');
    const imgs = document.getElementByClassName('img');
    for(let i = 0; i < imgs.length; i++){
        const img = imgs[i];
        // ...
        target.appendChild(img);
     }
        
}

上面的例子有两处可以优化的地方。

1.document是全局对象,位于作用域链的最外层,由于他在此函数中使用了不止一次,所以可以考虑将其声明为一个局部变量,以提升其在作用域链中的查找顺序。

2.计算css类名为.img的所有DOM节点数量的语句imgs.length执行了不止一遍。当查询符合条件的DOM节点列表存储到imgs中后,每次访问imgs.length时,DOM都会执行一次对页面元素的查找,可以考虑将长度值存储到局部变量。

优化后的代码写法如下:

function test(){
    const doc = document;
    const target = doc.getElementById('target');
    const imgs = doc.getElementByClassName('img');
    const n = imgs.length;
    for(let i = 0; i < n; i++){
        const img = imgs[i];
        // ...
        target.appendChild(img);
     }
        
}

4.用requestAnimationFrame方法控制渲染帧

requestAnimationFrame方法可以控制回调在两个渲染帧之间仅触发一次,如果在其回调函数中一开始就查找敏感属性的值,其实获取的是上一帧旧布局的值,并不会触发页面布局的重新计算。

// 在帧开始时触发回调
requestAnimationFrame(queryDivHeight);

function queryDivHeight(){
    const div = docuemnt.getElementById('div');
    
    console.log(div.offsetHeight);
}

总结

本文介绍了重排以及重绘带来的影响,分析了具体哪些行为会引起重排和重绘,并给出具体的规避方案,从而在这一方面优化性能。