如何加速你的 JavaScript【Part 4】:减少 DOM 操作

94 阅读4分钟

在之前的几篇文章中,我们讨论了如何通过优化循环、函数和递归等方式来提升 JavaScript 的性能。而在本系列的最后一篇文章中,我们将聚焦于 DOM 操作。众所周知,DOM 操作是非常缓慢的,往往是导致网页性能问题的主要原因之一。这篇文章将介绍一些减少 DOM 交互和优化重排(reflow)的技巧。

DOM 为什么慢?

DOM 操作之所以慢,主要原因在于每次对 DOM 进行修改,都会导致页面的重新渲染,而页面渲染(重排)是一个非常昂贵的操作。重排发生在以下几种情况下:

  • 添加或移除 DOM 节点。
  • 动态应用样式,例如 element.style.width = "10px"
  • 访问需要计算的属性值,如 offsetWidthclientHeight,或通过 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》