MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript优化DOM操作的性能

2023-08-164.1k 阅读

理解 DOM

在深入探讨如何优化 DOM 操作性能之前,我们首先需要对 DOM 有一个清晰的理解。DOM(文档对象模型)是一种将 HTML 或 XML 文档表示为树形结构的编程接口。在这个树形结构中,每个节点都是一个对象,包括文档本身、元素、属性、文本等。例如,对于以下简单的 HTML 代码:

<!DOCTYPE html>
<html>
<head>
    <title>DOM Example</title>
</head>
<body>
    <div id="main">
        <p class="content">This is a paragraph.</p>
    </div>
</body>
</html>

在 JavaScript 中,通过 document 对象我们可以访问整个文档的 DOM 结构。document.getElementById('main') 可以获取到 id 为 maindiv 元素节点,而 document.querySelector('p.content') 则能选中 classcontentp 元素。DOM 结构是与浏览器的渲染引擎紧密相关的,当我们对 DOM 进行操作时,浏览器需要重新计算元素的布局、样式,并重新渲染相关部分。这一系列操作都是比较消耗性能的。

DOM 操作为何影响性能

  1. 重排(Reflow)与重绘(Repaint)
    • 重排:当 DOM 的结构或几何属性(如元素的位置、大小、隐藏/显示状态等)发生改变时,浏览器需要重新计算元素在页面中的位置和大小等布局信息,这个过程称为重排。例如,当我们通过 JavaScript 改变一个元素的 width 属性时,浏览器需要重新计算该元素以及可能受其影响的其他元素的布局。重排是一个非常昂贵的操作,因为它涉及到整个文档或部分文档的布局重新计算。比如以下代码:
const div = document.createElement('div');
div.style.width = '100px';
document.body.appendChild(div);

这里创建一个新的 div 元素并添加到 body 中,这会导致浏览器对 body 及其子元素进行重排。

  • 重绘:当元素的外观(如颜色、背景色等)发生改变,但不影响其布局时,浏览器只需要重新绘制该元素,这个过程称为重绘。例如,改变一个元素的 color 属性:
const p = document.querySelector('p');
p.style.color ='red';

重绘的开销比重排小,但也会影响性能,尤其是当大量元素同时重绘时。

  1. 频繁访问和修改 DOM 每次访问 DOM 都需要从 JavaScript 环境进入到浏览器的渲染环境,这涉及到上下文的切换。如果在一个循环中频繁地访问和修改 DOM,会导致大量的重排和重绘操作,严重影响性能。例如:
const ul = document.getElementById('myList');
for (let i = 0; i < 100; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    ul.appendChild(li);
}

在这个例子中,每次循环都创建一个新的 li 元素并添加到 ul 中,这会导致 100 次重排操作,极大地影响性能。

优化 DOM 操作性能的方法

  1. 减少直接操作 DOM 的次数
    • 文档片段(DocumentFragment):文档片段是一个轻量级的 DOM 容器,它存在于内存中,对其进行操作不会引起页面的重排和重绘。当我们完成对文档片段的操作后,再将其添加到实际的 DOM 树中,这样只会触发一次重排和重绘。例如,改进上面的代码:
const ul = document.getElementById('myList');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    fragment.appendChild(li);
}
ul.appendChild(fragment);

通过使用文档片段,我们将 100 次对 ul 的操作合并为一次,大大提高了性能。

  • 批量修改样式:如果需要修改元素的多个样式属性,最好一次性修改,而不是逐个修改。例如:
const div = document.getElementById('myDiv');
// 不好的方式
div.style.width = '100px';
div.style.height = '100px';
div.style.backgroundColor = 'blue';
// 好的方式
div.style.cssText = 'width: 100px; height: 100px; background - color: blue;';

逐个修改样式会导致多次重排或重绘,而通过 cssText 一次性修改则只触发一次。但使用 cssText 时要注意可读性,并且要确保样式的正确性,避免覆盖掉其他重要的样式。

  1. 缓存 DOM 查询结果 每次调用 document.querySelectordocument.getElementById 等方法时,浏览器都需要在整个 DOM 树中进行查找。如果在代码中多次使用相同的查询,应该缓存查询结果。例如:
// 不好的方式
for (let i = 0; i < 10; i++) {
    const p = document.querySelector('p');
    p.textContent = `Count: ${i}`;
}
// 好的方式
const p = document.querySelector('p');
for (let i = 0; i < 10; i++) {
    p.textContent = `Count: ${i}`;
}

在第一个例子中,每次循环都进行一次 querySelector 查询,而在第二个例子中,只进行了一次查询并缓存了结果,提高了性能。

  1. 避免在布局信息改变时读取布局信息 有些 DOM 属性的读取会导致浏览器即时计算布局信息,例如 offsetTopclientWidth 等。如果在修改布局的同时读取这些属性,会强制浏览器进行额外的重排操作。例如:
const div = document.getElementById('myDiv');
// 不好的方式
for (let i = 0; i < 10; i++) {
    div.style.width = `${i * 10}px`;
    console.log(div.offsetTop);
}
// 好的方式
for (let i = 0; i < 10; i++) {
    div.style.width = `${i * 10}px`;
}
console.log(div.offsetTop);

在第一个例子中,每次修改 width 后又读取 offsetTop,这会导致额外的重排。而在第二个例子中,先完成所有的布局修改,再读取 offsetTop,减少了重排次数。

  1. 使用事件委托 事件委托是一种将事件处理程序绑定到父元素,而不是每个子元素的技术。当子元素触发事件时,事件会冒泡到父元素,父元素可以根据事件的目标来决定如何处理。例如,有一个包含多个 li 元素的 ul
<ul id="myList">
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
</ul>

如果为每个 li 元素单独绑定点击事件:

const lis = document.querySelectorAll('li');
lis.forEach(li => {
    li.addEventListener('click', function () {
        console.log(`Clicked: ${this.textContent}`);
    });
});

这样会为每个 li 元素都创建一个事件处理程序,当 li 元素数量较多时,会占用大量内存。使用事件委托:

const ul = document.getElementById('myList');
ul.addEventListener('click', function (event) {
    if (event.target.tagName === 'LI') {
        console.log(`Clicked: ${event.target.textContent}`);
    }
});

通过事件委托,只在 ul 元素上绑定了一个事件处理程序,大大减少了内存占用,并且提高了性能,尤其是当动态添加或删除 li 元素时,不需要重新绑定事件。

  1. 优化动画
    • 使用 CSS 动画:在可能的情况下,尽量使用 CSS 动画而不是 JavaScript 动画。CSS 动画由浏览器的合成线程处理,不会阻塞主线程,性能更好。例如,创建一个简单的淡入动画:
.fade - in {
    opacity: 0;
    animation: fade - in - anim 1s ease - in - out forwards;
}
@keyframes fade - in - anim {
    to {
        opacity: 1;
    }
}

然后在 JavaScript 中添加相应的类名来触发动画:

const element = document.getElementById('myElement');
element.classList.add('fade - in');
  • 使用 requestAnimationFrame:如果必须使用 JavaScript 实现动画,应该使用 requestAnimationFrame 方法。它会在浏览器下一次重绘之前调用指定的回调函数,确保动画与浏览器的刷新频率同步,避免不必要的重绘和性能浪费。例如,实现一个简单的元素移动动画:
<!DOCTYPE html>
<html>
<head>
    <style>
        #myDiv {
            position: relative;
            width: 50px;
            height: 50px;
            background - color: red;
        }
    </style>
</head>
<body>
    <div id="myDiv"></div>
    <script>
        const div = document.getElementById('myDiv');
        let x = 0;
        function move() {
            x += 1;
            div.style.left = `${x}px`;
            if (x < 100) {
                requestAnimationFrame(move);
            }
        }
        requestAnimationFrame(move);
    </script>
</body>
</html>

在这个例子中,requestAnimationFrame 确保动画流畅且性能良好。

  1. 合理使用 display: none 当一个元素的 display 属性设置为 none 时,该元素及其子元素不会在页面中渲染,并且不会占用布局空间。对隐藏元素进行操作不会触发重排和重绘。例如,如果需要对一个元素及其子元素进行大量修改,可以先将其设置为 display: none,修改完成后再显示:
const div = document.getElementById('myDiv');
div.style.display = 'none';
// 进行大量的 DOM 操作
const p = document.createElement('p');
p.textContent = 'New paragraph';
div.appendChild(p);
// 操作完成后显示
div.style.display = 'block';

这样可以减少在修改过程中触发的重排和重绘次数。

  1. 优化选择器 在使用 querySelectorquerySelectorAll 时,选择器的复杂度会影响查询性能。尽量使用简单、具体的选择器。例如:

    • 避免使用通配符选择器document.querySelectorAll('*') 会匹配文档中的所有元素,性能非常低,除非有特殊需求,应尽量避免使用。
    • 使用 ID 选择器document.getElementById 是性能最高的查询方式,因为 ID 在文档中是唯一的。如果能通过 ID 来定位元素,应优先使用。
    • 具体的类选择器document.querySelectorAll('.my - specific - class')document.querySelectorAll('div') 性能更好,因为类选择器更具体,浏览器可以更快地定位到目标元素。
  2. 虚拟 DOM 虚拟 DOM 是一种在 JavaScript 中模拟真实 DOM 结构的技术。它通过对比前后两次虚拟 DOM 的差异,然后将差异应用到真实 DOM 上,从而减少直接操作真实 DOM 的次数。虽然原生 JavaScript 没有内置虚拟 DOM 机制,但一些流行的前端框架(如 React、Vue 等)都使用了虚拟 DOM 技术。例如,在 React 中:

import React, { useState } from'react';

function App() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    );
}

export default App;

React 会在内部使用虚拟 DOM 来高效地更新页面。当点击按钮时,React 会计算新的虚拟 DOM 与旧的虚拟 DOM 的差异,然后只将差异部分应用到真实 DOM 上,避免了不必要的重排和重绘。

性能测试与分析

  1. 使用浏览器开发者工具 现代浏览器(如 Chrome、Firefox 等)都提供了强大的开发者工具来分析性能。在 Chrome 浏览器中,可以使用 Performance 面板来记录和分析页面的性能。
    • 录制性能数据:打开 Chrome 开发者工具,切换到 Performance 面板,点击录制按钮,然后在页面上进行需要测试的 DOM 操作(如添加元素、修改样式等),完成后停止录制。
    • 分析性能数据:录制完成后,会生成一个性能时间轴。在时间轴中,可以查看重排(Layout)、重绘(Paint)等操作的时间和次数。如果发现有大量的重排或重绘操作,就需要检查代码是否存在性能问题。例如,如果看到频繁的 Layout 操作,可能是在循环中频繁修改 DOM 布局导致的。
  2. 自定义性能测试代码 除了使用浏览器开发者工具,我们也可以编写自定义的性能测试代码来比较不同 DOM 操作方式的性能。例如,比较直接在循环中添加元素和使用文档片段添加元素的性能:
function testDirectAddition() {
    const ul = document.createElement('ul');
    const start = performance.now();
    for (let i = 0; i < 1000; i++) {
        const li = document.createElement('li');
        li.textContent = `Item ${i}`;
        ul.appendChild(li);
    }
    const end = performance.now();
    return end - start;
}

function testFragmentAddition() {
    const ul = document.createElement('ul');
    const fragment = document.createDocumentFragment();
    const start = performance.now();
    for (let i = 0; i < 1000; i++) {
        const li = document.createElement('li');
        li.textContent = `Item ${i}`;
        fragment.appendChild(li);
    }
    ul.appendChild(fragment);
    const end = performance.now();
    return end - start;
}

const directTime = testDirectAddition();
const fragmentTime = testFragmentAddition();
console.log(`Direct addition time: ${directTime} ms`);
console.log(`Fragment addition time: ${fragmentTime} ms`);

通过多次运行这些测试代码,并取平均值,可以更准确地比较不同方法的性能差异。

总结优化要点

  1. 减少直接操作 DOM 次数:利用文档片段、批量修改样式等方法,将多次操作合并为一次,降低重排和重绘次数。
  2. 缓存 DOM 查询结果:避免在循环或频繁使用的地方重复查询 DOM,提高查询效率。
  3. 避免布局信息冲突:在修改布局后再读取布局信息,减少不必要的重排。
  4. 使用事件委托:将事件处理程序绑定到父元素,减少事件处理程序数量,提高性能。
  5. 优化动画:优先使用 CSS 动画,若用 JavaScript 动画则结合 requestAnimationFrame
  6. 合理利用 display: none:对隐藏元素操作可减少重排和重绘。
  7. 优化选择器:使用简单、具体的选择器,避免复杂选择器。
  8. 考虑虚拟 DOM:在大型项目中借助框架的虚拟 DOM 技术提高性能。
  9. 性能测试与分析:通过浏览器开发者工具或自定义测试代码,发现并优化性能问题。

通过以上方法的综合应用,可以显著提高 JavaScript 中 DOM 操作的性能,提升用户体验,特别是在处理复杂的页面交互和动态内容时。在实际开发中,需要根据具体的场景和需求,灵活选择合适的优化策略。