前言
大家好,我是抹茶。
对于每个前端来说,重绘和重排都不是什么陌生的词汇,但是当我真的去专门研究前端性能优化的时候,会引起重绘和重排的行为远远超过我认知的那些,所以本文就将系统而具体的归纳总结会引起重绘和重排的行为。
一、重排和重绘的影响
重排是指重新计算布局,排列DOM,也就是从渲染流水线的布局树开始,重新走布局树=>图层树=>绘制=>合成的流程。
重绘是指重新绘画,也就是从渲染流水线的绘制开始,重新走绘制=>合成的流程。
二、引起重排和重绘的行为
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;
border;1px 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);
}
总结
本文介绍了重排以及重绘带来的影响,分析了具体哪些行为会引起重排和重绘,并给出具体的规避方案,从而在这一方面优化性能。