减少 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>注意事项
- 不要为了减少节点而牺牲语义:语义化 HTML 对 SEO 和无障碍更重要
- DocumentFragment 不持久化:添加到 DOM 后 fragment 会被清空
- CSS Containment 有兼容性:检查浏览器支持情况
- 虚拟滚动增加复杂度:只在列表超过几百项时使用
最佳实践
- 保持 DOM 节点数在合理范围内(< 1500)
- 使用 DocumentFragment 批量操作 DOM
- 避免强制同步布局(读写分离)
- 使用 transform/opacity 做动画
- 使用 CSS Containment 限制重排范围
- 对长列表使用虚拟滚动
- 使用 Chrome DevTools Performance 分析 DOM 操作
下一节
继续学习:Core Web Vitals