在之前的几篇文章中,我们讨论了如何通过优化循环、函数和递归等方式来提升 JavaScript 的性能。而在本系列的最后一篇文章中,我们将聚焦于 DOM 操作。众所周知,DOM 操作是非常缓慢的,往往是导致网页性能问题的主要原因之一。这篇文章将介绍一些减少 DOM 交互和优化重排(reflow)的技巧。
DOM 为什么慢?
DOM 操作之所以慢,主要原因在于每次对 DOM 进行修改,都会导致页面的重新渲染,而页面渲染(重排)是一个非常昂贵的操作。重排发生在以下几种情况下:
- 添加或移除 DOM 节点。
- 动态应用样式,例如
element.style.width = "10px"
。 - 访问需要计算的属性值,如
offsetWidth
、clientHeight
,或通过getComputedStyle()
或currentStyle
读取计算后的 CSS 值。
每次进行这些操作时,浏览器都需要重新计算页面布局,以确保最终结果是正确的。为了提升性能,我们的目标是减少页面中发生的重排次数。
避免频繁的 DOM 交互
首先,重要的一点是,大多数浏览器在 JavaScript 执行时不会立即更新 DOM。浏览器会将所有 DOM 操作排队,等到脚本执行完成后再一并应用。因此,我们可以通过减少对 DOM 的频繁操作来减少重排。
1. 使用文档片段(Document Fragment)
假设我们有一个循环,在每次迭代中向列表中添加一个新项,如下所示:
for (var i = 0; i < items.length; i++) {
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i));
list.appendChild(item);
}
这段代码的问题在于,每次迭代都会操作一次 DOM。这种频繁的 DOM 交互会触发多次重排。优化的方法是使用文档片段(document.createDocumentFragment()
)来作为中间容器,在循环中先将所有的 li
元素添加到文档片段中,最后一次性将文档片段添加到 DOM 中:
var fragment = document.createDocumentFragment();
for (var i = 0; i < items.length; i++) {
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i));
fragment.appendChild(item);
}
list.appendChild(fragment);
这样,整个过程只对 DOM 进行了一次操作,大大减少了重排的次数。
2. 暂时移除 DOM 节点
另一种减少重排的方法是,在进行批量 DOM 操作时,先将节点从 DOM 中移除,操作完成后再将其重新添加到 DOM 中。例如,下面的代码通过将列表的 display
样式设置为 none
来暂时移除它,然后再进行操作,最后恢复其显示状态:
list.style.display = "none";
for (var i = 0; i < items.length; i++) {
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i));
list.appendChild(item);
}
list.style.display = "";
通过这种方法,我们可以在进行多次 DOM 操作时,减少页面的重排。
优化样式变更
每次修改元素的样式属性都会导致页面的重排。如果对同一个元素进行多次样式更改,就会产生多次重排。例如:
element.style.backgroundColor = "blue";
element.style.color = "red";
element.style.fontSize = "12em";
这段代码会产生三次重排。更好的方法是将这些样式整合到一个 CSS 类中,然后通过 JavaScript 一次性切换类名来应用样式变更:
.newStyle {
background-color: blue;
color: red;
font-size: 12em;
}
element.className = "newStyle";
通过这种方式,只会触发一次重排,大大提高了性能,同时代码的可维护性也更高。
缓存 DOM 查询结果
频繁访问 DOM 也是性能问题的来源之一。每次访问 DOM 都是一个昂贵的操作,特别是当需要多次访问相同的元素时,应该尽量避免重复查询。例如,下面的代码非常低效:
document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +
document.getElementById("myDiv").offsetWidth + "px";
这段代码调用了三次 getElementById()
来访问同一个元素。优化的方法是将查询结果缓存到一个变量中,避免重复访问:
var myDiv = document.getElementById("myDiv");
myDiv.style.left = myDiv.offsetLeft + myDiv.offsetWidth + "px";
通过这种方式,我们减少了不必要的 DOM 查询,提升了性能。
避免 HTMLCollection 的多次访问
HTMLCollection
是一个动态集合,类似数组,但它会在每次访问时实时更新。例如,getElementsByTagName()
返回的就是一个 HTMLCollection
。如果在循环中频繁访问 HTMLCollection
的长度(length
属性),会导致性能问题,因为每次访问都会重新查询 DOM。
例如,下面的代码会导致无限循环:
var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++) {
document.body.appendChild(document.createElement("div"));
}
这是因为 divs.length
会随着每次添加新元素而增加,导致循环无法结束。优化的方式是缓存 length
属性的值,避免在每次循环中重新计算:
var divs = document.getElementsByTagName("div");
for (var i = 0, len = divs.length; i < len; i++) {
document.body.appendChild(document.createElement("div"));
}
通过缓存 length
,我们不仅避免了无限循环,还提高了代码的执行效率。
总结
减少 DOM 操作和优化重排是提升 JavaScript 性能的关键之一。通过使用文档片段、批量操作 DOM、缓存查询结果以及避免多次访问 HTMLCollection
,可以大幅提高页面的响应速度。在开发过程中,保持对 DOM 交互的关注,并采用这些优化技术,可以有效避免性能瓶颈。
至此,我们的“加速 JavaScript”系列到此结束。希望这些技巧能帮助你编写更加高效的代码!
参考文章: Zakas, Nicholas C.《Speed up your JavaScript, Part 4》