Skip to content

减少 DOM 节点数

DOM 规模直接影响页面的解析速度、布局计算、重排重绘和内存消耗。过多的 DOM 节点会导致页面卡顿、交互延迟和电池消耗增加。本节将介绍 DOM 规模对性能的影响、重排重绘优化策略,以及 DocumentFragment 等减少 DOM 操作的技术。

前置知识

阅读本节前,建议先了解:预加载与预连接

DOM 规模对性能的影响

DOM 节点数量参考

DOM 节点数性能评估说明
< 1,500优秀页面轻量,性能良好
1,500 - 3,000良好一般页面
3,000 - 5,000需关注较复杂页面
> 5,000需优化可能有性能问题

如何查看 DOM 节点数

Chrome DevTools → Performance → 录制 → Summary 中可以看到 DOM Nodes 数量。Elements 面板底部也显示节点总数。

DOM 规模的影响

方面影响
解析速度DOM 节点越多,HTML 解析时间越长
内存消耗每个 DOM 节点消耗内存(通常 1-2KB)
布局计算Layout 阶段需要遍历所有节点
重排重绘修改一个节点可能触发子树的重排
JavaScript 查询querySelector/querySelectorAll 在大 DOM 上更慢
GC 压力移除大量 DOM 节点增加垃圾回收压力

重排(Reflow)与重绘(Repaint)

触发重排的操作

javascript
// 以下操作触发重排(Layout)
element.style.width = '200px';
element.style.height = '100px';
element.style.marginTop = '10px';
element.style.padding = '10px';
element.style.display = 'none';
document.body.appendChild(newNode);
element.removeChild(childNode);
element.textContent = '新内容';

触发重绘的操作

javascript
// 以下操作仅触发重绘(Paint),不触发重排
element.style.color = 'red';
element.style.backgroundColor = '#f0f0f0';
element.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
element.style.textDecoration = 'underline';
element.style.outline = '2px solid blue';

最优操作:仅合成

javascript
// 以下操作仅触发合成(Composite),最高效
element.style.transform = 'translateX(100px)';
element.style.transform = 'scale(1.5)';
element.style.opacity = '0.5';
element.style.filter = 'blur(5px)';

优化策略

批量 DOM 操作

html
<!-- 错误:逐个添加元素 -->
<ul id="list"></ul>
<script>
  // 每次添加都触发重排
  const list = document.getElementById('list');
  for (let i = 0; i < 1000; i++) {
    const li = document.createElement('li');
    li.textContent = `项目 ${i}`;
    list.appendChild(li);  // 触发 1000 次重排
  }
</script>

<!-- 正确:使用 DocumentFragment 批量添加 -->
<ul id="list"></ul>
<script>
  const list = document.getElementById('list');
  const fragment = document.createDocumentFragment();

  for (let i = 0; i < 1000; i++) {
    const li = document.createElement('li');
    li.textContent = `项目 ${i}`;
    fragment.appendChild(li);  // 不触发重排(fragment 不在 DOM 中)
  }

  list.appendChild(fragment);  // 只触发 1 次重排
</script>

使用 innerHTML 替代 createElement

javascript
// 对于大量简单内容,innerHTML 更快
const list = document.getElementById('list');
let html = '';
for (let i = 0; i < 1000; i++) {
  html += `<li>项目 ${i}</li>`;
}
list.innerHTML = html;  // 只触发 1 次重排

避免强制同步布局

javascript
// 错误:读取布局属性后修改样式(强制同步布局)
element.style.width = '100px';     // 写入
const height = element.offsetHeight; // 读取(强制布局计算)
element.style.height = height + 20; // 写入(再次强制布局计算)

// 正确:批量读取,再批量写入
const width = element.offsetWidth;   // 读取
const height = element.offsetHeight; // 读取
element.style.width = width + 20 + 'px';   // 写入
element.style.height = height + 20 + 'px'; // 写入

使用 requestAnimationFrame

javascript
// 使用 rAF 确保在下一帧渲染前执行
function updateLayout() {
  // 批量修改 DOM
  element.style.transform = 'translateX(100px)';
  element.style.opacity = '0.5';
}

// 在下一帧渲染前执行,合并多次调用
requestAnimationFrame(updateLayout);

使用 CSS Containment

css
/* CSS Containment 限制重排范围 */
.card {
  contain: layout;
  /* 告诉浏览器此元素的布局变化不影响外部 */
}

.sidebar {
  contain: content;
  /* 包含 layout、paint 和 size */
}

DocumentFragment

DocumentFragment 是一个轻量级的 DOM 容器,不属于活动文档的一部分,在其中进行 DOM 操作不会触发重排重绘:

javascript
// DocumentFragment 示例
function renderList(items) {
  const fragment = document.createDocumentFragment();

  items.forEach(item => {
    const div = document.createElement('div');
    div.className = 'item';
    div.innerHTML = `
      <h3>${item.title}</h3>
      <p>${item.description}</p>
    `;
    fragment.appendChild(div);
  });

  // 一次性添加到 DOM
  document.getElementById('container').appendChild(fragment);
}

减少不必要的 DOM 节点

避免深层嵌套

html
<!-- 不推荐:过度嵌套 -->
<div class="wrapper">
  <div class="container">
    <div class="row">
      <div class="col">
        <div class="card">
          <div class="card-body">
            <div class="card-content">
              <p>内容</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- 推荐:合理嵌套 -->
<div class="card">
  <div class="card-body">
    <p>内容</p>
  </div>
</div>

虚拟滚动

对于长列表,使用虚拟滚动只渲染可视区域内的元素:

html
<!-- 虚拟滚动列表 -->
<div id="virtual-list" style="height: 500px; overflow: auto;">
  <!-- 只渲染可见的 10-20 个项目 -->
  <div class="item" style="height: 40px;">项目 1</div>
  <div class="item" style="height: 40px;">项目 2</div>
  <!-- ... -->
</div>

<script>
  // 虚拟滚动实现
  const container = document.getElementById('virtual-list');
  const ITEM_HEIGHT = 40;
  const VISIBLE_COUNT = Math.ceil(container.clientHeight / ITEM_HEIGHT) + 5;

  function render(startIndex) {
    const fragment = document.createDocumentFragment();
    const endIndex = Math.min(startIndex + VISIBLE_COUNT, totalItems);

    for (let i = startIndex; i < endIndex; i++) {
      const div = document.createElement('div');
      div.className = 'item';
      div.style.height = ITEM_HEIGHT + 'px';
      div.textContent = `项目 ${i + 1}`;
      fragment.appendChild(div);
    }

    container.innerHTML = '';
    container.appendChild(fragment);
  }

  container.addEventListener('scroll', () => {
    const startIndex = Math.floor(container.scrollTop / ITEM_HEIGHT);
    render(startIndex);
  });
</script>

注意事项

  1. 不要为了减少节点而牺牲语义:语义化 HTML 对 SEO 和无障碍更重要
  2. DocumentFragment 不持久化:添加到 DOM 后 fragment 会被清空
  3. CSS Containment 有兼容性:检查浏览器支持情况
  4. 虚拟滚动增加复杂度:只在列表超过几百项时使用

最佳实践

  • 保持 DOM 节点数在合理范围内(< 1500)
  • 使用 DocumentFragment 批量操作 DOM
  • 避免强制同步布局(读写分离)
  • 使用 transform/opacity 做动画
  • 使用 CSS Containment 限制重排范围
  • 对长列表使用虚拟滚动
  • 使用 Chrome DevTools Performance 分析 DOM 操作

下一节

继续学习:Core Web Vitals

参考链接