高性能JavaScript:深入理解重排与重绘优化

作者:极速慢跑

时间:2026-10-29

浏览:593

本文深入解析JavaScript中DOM重排(Reflow)与重绘(Repaint)的机制及其对性能的消耗。通过对比实验数据,揭示了频繁访问DOM引发的性能瓶颈,并详细阐述了触发重排的具体场景。文章提供了多种优化策略,包括批量修改样式、利用DocumentFragment批量添加节点以及通过绝对定位使元素脱离文档流,旨在帮助开发者编写更高效、响应更快的Web应用。

文章目录

引言:DOM编程的性能挑战

回顾前文关于高性能JavaScript DOM编程的内容,核心优化策略主要包含两点:一是尽可能减少DOM访问,将计算逻辑移至ECMAScript层面;二是缓存局部变量(如length属性等)。此外,文章还介绍了querySelector()和querySelectorAll()两个新API,在组合选择器场景中建议优先使用。本文重点探讨DOM编程中最为耗时的环节:重排与重绘。

引言:DOM编程的性能挑战

重排与重绘的基本概念

1、什么是重排和重绘 浏览器在加载完页面所有组件(HTML标记、JavaScript、CSS、图片)后,会解析生成两个内部数据结构:DOM树和渲染树。

DOM树用于表示页面结构,而渲染树则展示DOM节点的具体显示方式。DOM树中每个需要显示的节点在渲染树中至少对应一个节点(display值为none的隐藏DOM元素在渲染树中无对应节点)。渲染树中的节点被称为“帧”或“盒”,遵循CSS模型定义,即页面元素被视为包含填充、边距、边框和位置的盒子。DOM和渲染树构建完成后,浏览器开始绘制页面元素。

当DOM变化影响元素的几何属性(宽或高)时,浏览器需重新计算该元素的几何属性,这同样会影响其他元素的几何属性和位置。浏览器会使渲染树中受影响部分失效,并重新构造渲染树,此过程称为重排。重排完成后,浏览器将受影响部分重新绘制到屏幕,称为重绘。由于浏览器的流式布局特性,渲染树计算通常只需遍历一次即可完成。但table及其内部元素除外,它可能需要多次计算才能确定渲染树中节点的属性,耗时通常是同等元素的3倍。这也是避免使用table进行布局的原因之一。

并非所有DOM变化都会影响几何属性。例如,改变元素背景色不会影响其宽高,这种情况下仅发生重绘。

重排与重绘的性能代价分析

2、重排和重绘的代价究竟多大 重排和重绘的性能代价有多大?回顾前文中的“过桥”案例,细心的读者可能已发现,千倍的时间差异并非仅由“过桥”操作造成。每次“过桥”都伴随着重排和重绘,而绝大部分能耗正源于此!


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所耗时间几乎可以忽略不计。

触发重排的场景

3、重排何时发生 显然,每次重排必然导致重绘。那么,重排会在哪些情况下发生?

1、添加或删除可见的DOM元素 2、元素位置改变 3、元素尺寸改变 4、元素内容改变(例如:一个文本被另一个不同尺寸的图片替代) 5、页面渲染初始化(此过程无法避免) 6、浏览器窗口尺寸改变 这些情况显而易见。或许你已有过这样的体验:不间断地改变浏览器窗口大小时,UI反应变得迟钝(在某些低版本IE中甚至直接崩溃)。现在你可能恍然大悟,没错,正是由一次次的重排和重绘导致的!

渲染树变化的排队与刷新机制

4、渲染树变化的排队和刷新 思考以下代码:


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

乍看之下,元素样式改变了三次,每次改变都会引起重排和重绘,因此总共有三次重排重绘过程。但浏览器并不会如此低效,它会将三次修改“保存”起来(大多数浏览器通过队列化修改并批量执行来优化重排过程),一次性完成!然而,有时你可能会(往往是不知不觉地)强制刷新队列并要求计划任务立即执行。获取布局信息的操作会导致队列刷新,例如:

1.offsetTop, offsetLeft, offsetWidth, offsetHeight 2.scrollTop, scrollLeft, scrollWidth, scrollHeight 3.clientTop, clientLeft, clientWidth, clientHeight 4.getComputedStyle() (IE中为currentStyle) 将上述代码稍作修改:


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

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

由于offsetHeight属性需要返回最新的布局信息,浏览器不得不执行渲染队列中的“待处理变化”并触发重排以返回正确值(即使队列中改变的样式属性与想要获取的属性值并无关系)。因此,上述代码中,前两次的操作会缓存在渲染队列中待处理,但一旦请求offsetHeight属性,队列就会立即执行,导致总共发生两次重排与重绘。因此,尽量避免在布局信息改变时进行查询。

最小化重排与重绘的优化策略

5、最小化重排和重绘 我们再次查看上述代码:


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

三个样式属性被改变,每个都会影响元素的几何结构。虽然大部分现代浏览器已做优化,仅引起一次重排,但如上文所述,若请求了一个及时属性,则会强制刷新队列。此外,这段代码访问了四次DOM。一个显而易见的优化策略是将操作合并为一次,从而只修改DOM一次:


var ele = document.getElementById('myDiv');

// 1. 重写style
ele.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

// 2. add style
ele.style.cssText += 'border-;eft: 1px;'

// 3. use class
ele.className = 'active';

DocumentFragment的应用

6、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。

使元素脱离动画流的技巧

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

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

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

使用绝对位置定位页面上的动画元素,将其脱离文档流 让元素动起来。当它扩大时,会临时覆盖部分页面。但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容。 当动画结束时恢复定位,从而只会下移一次文档的其他元素 总结 重排和重绘是DOM编程中耗能的主要原因之一,平时涉及DOM编程时可以参考以下几点:

总结与最佳实践

尽量不要在布局信息改变时做查询(会导致渲染队列强制刷新) 同一个DOM的多个属性改变可以写在一起(减少DOM访问,同时把强制渲染队列刷新的风险降为0) 如果要批量添加DOM,可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排(fragment元素的应用) 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

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

希望这两篇文章可以帮到大家,解决大家这方面的疑惑。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章