您的位置:首页 > Web前端 > JavaScript

高性能JavaScript之重绘与重排

2016-03-23 10:09 519 查看

浏览器是怎样渲染一个页面的?

1,解析标签DOM:浏览器解析html源码,然后创建一个 DOM树。

在DOM树中,每一个HTML标签都有一个对应的节点,并且每一个文本也都会有一个对应的文本节点。

DOM树的根节点就是 documentElement,对应的是html标签。

2,渲染:浏览器解析CSS代码,计算出最终的样式数据。对CSS代码中非法的语法她会直接忽略掉。解析CSS的时候会按照如下顺序来定义优先级:浏览器默认设置,用户设置,外链样式,内联样式,html中的style。

3,布局:构建出DOM树,并且计算出样式数据后,下一步就是构建一个 渲染树(rendering tree)。渲染树和DOM树有点像,但是是有区别的。DOM树完全和html标签一一对应,但是渲染树会忽略掉不需要渲染的元素,比如head、display:none的元素等。渲染树中的每一个节点都存储有对应的css属性。

4,绘制:一旦渲染树创建好了,浏览器就可以根据渲染树直接把页面绘制到屏幕上。

2、什么是重排和重绘

重排:当页面上的改变影响了文档内容、结构或者元素定位时,就会发生重排(或称“重新布局”),就是相当于渲染树需要重新计算…..。

重绘:当在页面上修改了一些不需要改变定位的样式的时候(比如background-color,border-color,visibility),浏览器只会将新的样式重新绘制给元素(这就叫一次“重绘”或者“重新定义样式”)。

重绘是一个元素外观的改变所触发的浏览器行为,例如改变visibility、outline、背景色等属性。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,并不一定伴随重排。

重排是更明显的一种改变,可以理解为渲染树需要重新计算。重排的代价更大。

3、重排和重绘的代价究竟多大

重排和重绘的代价有多大?我们再回到前文那个过桥的例子上,细心的你可能会发现了,千倍的时间差并不是由于“过桥”一手造成的,每次“过桥”其实都伴随着重排和重绘,而耗能的绝大部分也正是在这里!

var times = 15000;

// code1 每次过桥+重排+重绘
console.time(1);
for(var i = 0; i < times; i++) {
document.getElementById('myDiv1').innerHTML += 'a';
}
console.timeEnd(1);

// code2 只过桥
console.time(2);
var str = '';
for(var i = 0; i < times; i++) {
var tmp = document.getElementById('myDiv2').innerHTML;
str += 'a';
}
document.getElementById('myDiv2').innerHTML = str;
console.timeEnd(2);

// code3
console.time(3);
var _str = '';
for(var i = 0; i < times; i++) {
_str += 'a';
}
document.getElementById('myDiv3').innerHTML = _str;
console.timeEnd(3);

// 1: 2874.619ms
// 2: 11.154ms
// 3: 1.282ms


数据是不会撒谎的,看到了吧,多次访问DOM对于重排和重绘来说,耗时简直不值一提了。

4、重排何时发生

很显然,每次重排,必然会导致重绘,那么,重排会在哪些情况下发生?

1、添加或者删除可见的DOM元素(DOM结构)

2、元素位置改变(DOM结构)

3、元素尺寸改变(DOM几何属性)

4、元素内容改变(例如:一个文本被另一个不同尺寸的图片替代)

5、页面渲染初始化(这个无法避免)

6、浏览器窗口尺寸改变

这些都是显而易见的,或许你已经有过这样的体会,不间断地改变浏览器窗口大小,导致UI反应迟钝(某些低版本IE下甚至直接挂掉),现在你可能恍然大悟,没错,正是一次次的重排重绘导致的!

4.1. DOM元素的几何属性变化

当DOM元素的几何属性变化时,渲染树中的相关节点就会失效,浏览器会根据DOM元素的变化重新构建渲染树中失效的节点。之后,会根据新的渲染树重新绘制这部分页面。而且,当前元素的重排也许会带来相关元素的重排。例如,容器节点的渲染树改变时,会触发子节点的重新计算,也会触发其后续兄弟节点的重排,祖先节点需要重新计算子节点的尺寸也会产生重排。最后,每个元素都将发生重绘。可见,重排一定会引起浏览器的重绘,一个元素的重排通常会带来一系列的反应,甚至触发整个文档的重排和重绘,性能代价是高昂的。

4.2 DOM树的结构变化

当DOM树的结构变化时,例如节点的增减、移动等,也会触发重排。浏览器引擎布局的过程,类似于树的前序遍历,是一个从上到下从左到右的过程。通常在这个过程中,当前元素不会再影响其前面已经遍历过的元素。所以,如果在body最前面插入一个元素,会导致整个文档的重新渲染,而在其后插入一个元素,则不会影响到前面的元素。

4.3 获取某些属性

浏览器引擎可能会针对重排做了优化。比如Opera,它会等到有足够数量的变化发生,或者等到一定的时间,或者等一个线程结束,再一起处理,这样就只发生一次重排。但除了渲染树的直接变化,当获取一些属性时,浏览器为取得正确的值也会触发重排。这样就使得浏览器的优化失效了。这些属性包括:

offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
getComputedStyle(), or currentStyle in IE


5、渲染树变化的排队和刷新

思考下面代码:

var $body = $('body');
$body.css('padding', '1px');
// 重排, 重绘
$body.css('color', 'red');
// 重绘
$body.css('margin', '2px');
// 重排, 重绘
// 实际上只有一次重排和重绘被执行。


乍一想,元素的样式改变了三次,每次改变都会引起重排和重绘,所以总共有三次重排重绘过程,但是浏览器并不会这么笨,它会把三次修改“保存”起来(大多数浏览器通过队列化修改并批量执行来优化重排过程),一次完成!但是,有些时候你可能会(经常是不知不觉)强制刷新队列并要求计划任务立即执行。获取布局信息的操作会导致队列刷新,比如:4.3 获取某些属性

将上面的代码稍加修改:

var $body = $('body');
$body.css('padding', '1px');
$body.css('padding');
// 这里读取了一次元素的属性,一次强制重排就会发生。
$body.css('color', 'red');
$body.css('margin', '2px');


上面这段代码的结果就是,进行了两次重排。因此,为了提高性能,你应该讲读取元素属性的代码组织在一起

var ele = document.getElementById('myDiv');
ele.style.borderLeft = '1px';
ele.style.borderRight = '2px';

// here use offsetHeight
// ...
ele.style.padding = '5px';


因为offsetHeight属性需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并触发重排以返回正确的值(即使队列中改变的样式属性和想要获取的属性值并没有什么关系),所以上面的代码,前两次的操作会缓存在渲染队列中待处理,但是一旦offsetHeight属性被请求了,队列就会立即执行,所以总共有两次重排与重绘。所以尽量不要在布局信息改变时做查询。

6、最小化重排和重绘

开发中,比较好的实践是尽量减少重排次数和缩小重排的影响范围。例如:

将多次改变样式属性的操作合并成一次操作。例如,

var changeDiv = document.getElementById(‘changeDiv’);
changeDiv.style.color = ‘#093′;
changeDiv.style.background = ‘#eee’;
changeDiv.style.height = ’200px’;


  

  可以合并为:CSS:

div.changeDiv {
background: #eee;
color: #093;
height: 200px;
}


  JS操作:

document.getElementById(‘changeDiv’).className = ‘changeDiv’;


2、 由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。

3、 在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的html片段,再一次性添加到文档中去,而不是循环添加每一行。

7、fragment元素的应用

看如下代码,考虑一个问题:

<ul id='fruit'>
<li> apple </li>
<li> orange </li>
</ul>


如果代码中要添加内容为peach、watermelon两个选项,你会怎么做?

var lis = document.getElementById('fruit');
var li = document.createElement('li');
li.innerHTML = 'apple';
lis.appendChild(li);

var li = document.createElement('li');
li.innerHTML = 'watermelon';
lis.appendChild(li);


很容易想到如上代码,但是很显然,重排了两次,怎么破?前面我们说了,隐藏的元素不在渲染树中,太棒了,我们可以先把id为fruit的ul元素隐藏(display=none),然后添加li元素,最后再显示,但是实际操作中可能会出现闪动,原因这也很容易理解。这时,fragment元素就有了用武之地了。

var fragment = document.createDocumentFragment();

var li = document.createElement('li');
li.innerHTML = 'apple';
fragment.appendChild(li);

var li = document.createElement('li');
li.innerHTML = 'watermelon';
fragment.appendChild(li);

document.getElementById('fruit').appendChild(fragment);


文档片段是个轻量级的document对象,它的设计初衷就是为了完成这类任务——更新和移动节点。文档片段的一个便利的语法特性是当你附加一个片断到节点时,实际上被添加的是该片断的子节点,而不是片断本身。只触发了一次重排,而且只访问了一次实时的DOM。

8、让元素脱离动画流

用展开/折叠的方式来显示和隐藏部分页面是一种常见的交互模式。它通常包括展开区域的几何动画,并将页面其他部分推向下方。

一般来说,重排只影响渲染树中的一小部分,但也可能影响很大的部分,甚至整个渲染树。浏览器所需要重排的次数越少,应用程序的响应速度就越快。因此当页面顶部的一个动画推移页面整个余下的部分时,会导致一次代价昂贵的大规模重排,让用户感到页面一顿一顿的。渲染树中需要重新计算的节点越多,情况就会越糟。

使用以下步骤可以避免页面中的大部分重排:

将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

9、总结

重排和重绘是DOM编程中耗能的主要原因之一,平时涉及DOM编程时可以参考以下几点:

尽量不要在布局信息改变时做查询(会导致渲染队列强制刷新)

同一个DOM的多个属性改变可以写在一起(减少DOM访问,同时把强制渲染队列刷新的风险降为0)

如果要批量添加DOM,可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排(fragment元素的应用)

将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

以上就是高性能JavaScript 重排与重绘的全部介绍内容,大家可以结合上一篇高性能JavaScript之DOM编程一起学习,

希望这两篇文章可以帮到大家,解决大家这方面的疑惑。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息