操作 DOM 为什么这么慢?

因为操作DOM要收过路费

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》

浏览器内核中的JS 引擎和渲染引擎是独立存在的,当我们用JS去操作DOM时,本质上是JS引擎和渲染引擎之间进行的“跨界交流”。

过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。

回流和重绘

JS操作DOM很慢,而且修改DOM还会引发它的外观、样式、大小、位置的改变,就会触发重流重绘

  • 回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。

  • 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。

由此可以看出,重绘不一定导致回流,但是回流一定会导致重绘。

优化的关键,就是把重绘和回流的次数最小化。

如何优化

知道了DOM慢的原因,我就可以逐步优化了。

减少DOM操作,将多次操作合并为一次。

假如,我们要将1-1000渲染到box里,如下:

<div id="box"></div>
1

我们首先想到的可能会这样:

for (var i = 1; i <= 1000; i++) {
    document.getElementById('box').innerHTML += `<p>${i}</p>`
}
1
2
3

虽然最终效果是可以实现的,但是每次for循环都要过一次“桥”,一次两次还好,如果操作几十万、几百万的数据呢?页面就会明显感到卡顿..

能不能优化呢?当然可以,我们可以先设置一个变量,先去操作这个变量,最后一次插入DOM。

var str = ''
for (var i = 1; i <= 1000; i++) {
    str += `<p>${i}</p>`
}
document.getElementById('box').innerHTML = str;
1
2
3
4
5

将多次DOM操作整合为一次,这也是一种优化的方式。

使用DocumentFragment

将DOM多次修改,最后一次性插入到DOM中,这点在DocumentFragment中展现的淋漓尽致,关于DocumentFragment详细操作可以下一章。