前端性能优化之重排和重绘
内容转载于思否:
⼀、重排 & 重绘
有经验的⼤佬对这个概念⼀定不会陌⽣,“浏览器输⼊URL发⽣了什么”。估计⼤家已经烂熟于⼼了,从计算机⽹络到JS引擎,⼀路飞奔到浏览器渲染引擎。经验越多就能理解的越深。
感兴趣的同学可以看⼀下这篇⽂章,深度和⼴度俱佳:
切回正题,我们继续探讨何为重排。浏览器下载完页⾯所有的资源后,就要开始构建DOM树,与此同时还会构建渲染树(Render Tree)。(其实在构建渲染树之前,和DOM树同期会构建Style Tree。DOM树与Style Tree合并为渲染树)
树:表⽰页⾯的结构
DOM树:
渲染树:表⽰页⾯的节点如何显⽰
渲染树:
⼀旦渲染树构建完成,就要开始绘制(paint)页⾯元素了。
当DOM的变化引发了元素⼏何属性的变化,⽐如改变元素的宽⾼,元素的位置,导致浏览器不得不重新计算元素的⼏何属性,并重新构建渲染树,这个过程称为“重排”。完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程就是“重绘”。
简单的说,重排负责元素的⼏何属性更新,重绘负责元素的样式更新。⽽且,重排必然带来重绘,但是重绘未必带来重排。⽐如,改变某个元素的背景,这个就不涉及元素的⼏何属性,所以只发⽣重绘。
⼆、重排触发机制绘
元素的⼏何属性发⽣了改变,那么我们就从能够改变元素⼏何属性的⾓度⼊⼿
上⾯已经提到了,重排发⽣的根本原理就是元素的⼏何属性
1. 添加或删除可见的DOM元素
2. 元素位置改变
3. 元素本⾝的尺⼨发⽣改变
4. 内容改变
5. 页⾯渲染器初始化
6. 浏览器窗⼝⼤⼩发⽣改变
三、如何进⾏性能优化
重绘和重排的开销是⾮常昂贵的,如果我们不停的在改变页⾯的布局,就会造成浏览器耗费⼤量的开销在进⾏页⾯的计算,这样的话,我们页⾯在⽤户使⽤起来,就会出现明显的卡顿。现在的浏览器其实已经对重排进⾏了优化,⽐如如下代码:
var div = document.querySelector('.div');
div.style.width = '200px';
div.style.background = 'red';
div.style.height = '300px';
⽐较久远的浏览器,这段代码会触发页⾯2次重排,在分别设置宽⾼的时候,触发2次.
当代的浏览器对此进⾏了优化,这种思路类似于现在流⾏的MVVM框架使⽤的虚拟DOM,对改变的DOM节点进⾏依赖收集,确认没有改变的节点,就进⾏⼀次更新。但是浏览器针对重排的优化虽然思路和虚拟DOM接近,但是还是有本质的区别。⼤多数浏览器通过队列化修改并批量执⾏来优化重排过程。也就是说上⾯那段代码其实在现在的浏览器优化下,只构成⼀次重排。
但是还是有⼀些特殊的元素⼏何属性会造成这种优化失效。⽐如:
offsetTop, offsetLeft,...
scrollTop, scrollLeft, ...
clientTop, clientLeft, ...
getComputedStyle() (currentStyle in IE)
为什么造成优化失效呢?仔细看这些属性,都是需要实时回馈给⽤户的⼏何属性或者是布局属性,当然不能再依靠浏览器的优化,因此浏览器不得不⽴即执⾏渲染队列中的“待处理变化”,并随之触发重排返回正确的值。
接下来深⼊的介绍⼏种性能优化的⼩TIPS
3.1 最⼩化重绘和重排
既然重排&重绘是会影响页⾯的性能,尤其是糟糕的JS代码更会将重排带来的性能问题放⼤。既然如此,我们⾸先想到的就是减少重排重绘。
考虑下⾯这个例⼦:
// javascript
var el = document.querySelector('.el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
这个例⼦其实和上⾯那个例⼦是⼀回事⼉,在最糟糕的情况下,会触发浏览器三次重排。然鹅更⾼效的⽅式就是合并所有的改变⼀次处理。这样就只会修改DOM节点⼀次,⽐如改为使⽤cssText属性实现:
var el = document.querySelector('.el');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';
沿着这个思路,聪明的⽼铁⼀定就说了,你直接改个类名不也妥妥的。没错,还有⼀种减少重排的⽅法就是切换类名,⽽不是使⽤内联样式的cssText⽅法。使⽤切换类名就变成了这样:
// css
.active {
padding: 5px;
border-left: 1px;
border-right: 2px;
}
// javascript
var el = document.querySelector('.el');
el.className = 'active';
批量修改DOM
如果我们需要对DOM元素进⾏多次修改,怎么去减少重排和重绘的次数呢?
有的同学⼜要说了,利⽤上⾯修改样式的⽅法不就⾏了吗。回过头看⼀下造成页⾯重排的⼏个要点⾥,可以明确的看到,造成元素⼏何属性发⽣改变就会触发重排,现在需要增加10个节点,必然涉及到DOM的修改,这个时候就需要利⽤批量修改DOM这种优化⽅式了,这⾥也能看到,改变样式最⼩化重绘和重排这种优化⽅式适⽤于单个存在的节点。
批量修改DOM元素的核⼼思想是:
让该元素脱离⽂档流
对其进⾏多重改变
将元素带回⽂档中
打个⽐⽅,我们主机硬盘出现了故障,常见的办法就是把硬盘卸下来,⽤专业的⼯具测试哪⾥有问题,待修复后再安装上去。要是直接在主板上⾯⽤螺丝⼑弄来弄去,估计主板⼀会⼉也要坏了...
这个过程引发俩次重排,第⼀步和第三步,如果没有这两步,可以想象⼀下,第⼆步每次对DOM的增删都会引发⼀次重排。那么知道批量修改DOM的核⼼思想后,我们再了解三种可以使元素可以脱离⽂档流的⽅法,注意,这⾥不使⽤css中的浮动&绝对定位,这是风马⽜不相及的概念。
看⼀下下⾯这个代码⽰例:
// html
<ul id="mylist">
<li><a href="www.mi">xiaomi</a></li>
<li><a href="www.miui">miui</a></li>
</ul>
// javascript 现在需要添加带有如下信息的li节点
let data = [
{
name: 'tom',
url: 'www.baidu',
},
{
name: 'ann',
url: 'hFE'
}
]
⾸先,我们先写⼀个通⽤的⽤于将新数据更新到指定节点的⽅法:
function appendNode($node, data) {
var a, li;
for(let i = 0, max = data.length; i < max; i++) {
a = ateElement('a');
li = ateElement('li');
a.href = data[i].url;
a.ateTextNode(data[i].name));
li.appendChild(a);
$node.appendChild(li);
}
}
⾸先我们忽视所有的重排因素,⼤家肯定会这么写:
let ul = document.querySelector('#mylist');
appendNode(ul, data);
使⽤这种⽅法,在没有任何优化的情况下,每次插⼊新的节点都会造成⼀次重排(这⼏部分我们都先讨论重排,因为重排是性能优化的第⼀步)。
考虑这个场景,如果我们添加的节点数量众多,⽽且布局复杂,样式复杂,那么能想到的是你的页⾯⼀定⾮常卡顿。我们利⽤批量修改DOM的优化⼿段来进⾏重构
1)隐藏元素,进⾏修改后,然后再显⽰该元素
let ul = document.querySelector('#mylist');
ul.style.display = 'none';
appendNode(ul, data);
ul.style.display = 'block';
这种⽅法造成俩次重排,分别是控制元素的显⽰与隐藏。对于复杂的,数量巨⼤的节点段落可以考虑这种⽅法。为啥使⽤display属性呢,因为display为none的时候,元素就不在⽂档流了,还不熟悉的⽼铁,⼿动Google⼀下三者的区别
display:none;
opacity: 0;
visibility: hidden
2)使⽤⽂档⽚段创建⼀个⼦树,然后再拷贝到⽂档中
let fragment = ateDocumentFragment();
appendNode(fragment, data);
ul.appendChild(fragment);
我是⽐较喜欢这种⽅法的,⽂档⽚段是⼀个轻量级的document对象,它设计的⽬的就是⽤于更新,移动节点之类的任务,⽽且⽂档⽚段还有⼀个好处就是,当向⼀个节点添加⽂档⽚段时,添加的是⽂档⽚段的⼦节点,⾃⾝不会被添加进去。
不同于第⼀种⽅法,这个⽅法并不会使元素短暂消失造成逻辑问题。上⾯这个例⼦,只在添加⽂档⽚段的时候涉及到了⼀次重排。
3)将原始元素拷贝到⼀个独⽴的节点中,操作这个节点,然后覆盖原始元素
let old = document.querySelector('#mylist');
let clone = old.cloneNode(true);
appendNode(clone, data);
placeChild(clone, old);
可以看到这种⽅法也是只有⼀次重排。总的来说,使⽤⽂档⽚段,可以操作更少的DOM(对⽐使⽤克隆节点),最⼩化重排重绘次数。
缓存布局信息
缓存布局信息这个概念,在《⾼性能JavaScript》DOM性能优化中,多次提到类似的思想.
⽐如我现在要得到页⾯ul节点下⾯的100个li节点,最好的办法就是第⼀次获取后就保存起来,减少DOM的访问以提升性能,缓存布局信息也是同样的概念。
前⾯有讲到,当访问诸如offsetLeft,clientTop这种属性时,会冲破浏览器⾃有的优化————通过队列
化修改和批量运⾏的⽅法,减少重排/重绘版次。所以我们应该尽量减少对布局信息的查询次数,查询时,将其赋值给局部变量,使⽤局部变量参与计算。
看以下样例:
将元素div向右下⽅平移,每次移动1px,起始位置100px, 100px。性能糟糕的代码:
div.style.left = 1 + div.offsetLeft + 'px';
p = 1 + div.offsetTop + 'px';
这样造成的问题就是,每次都会访问div的offsetLeft,造成浏览器强制刷新渲染队列以获取最新的offsetLeft值。更好的办法就是,将这个值保存下来,避免重复取值
current = div.offsetLeft;
div.style.left = 1 + ++current + 'px';
p = 1 + ++current + 'px';

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。