3、渲染引擎基础:DOM操作与性能、DocumentFragment的使用、虚拟DOM与增量渲染的概念

好,咱们今天聊点实在的。渲染引擎这块,说白了就是浏览器怎么把HTML变成你看到的页面。我刚开始写前端那会儿,觉得DOM操作不就是改改内容嘛,有啥难的?后来项目一复杂,页面卡成PPT,才意识到这里头水很深。

3.1 DOM操作与性能:别让浏览器累着

DOM操作是前端最基础的操作,但也是最容易踩坑的地方。你想想看,每次你用document.getElementById或者innerHTML改东西,浏览器都得重新计算布局、重新绘制。改一次还好,改一百次呢?

我遇到过最夸张的一个项目,有个同事在循环里直接操作DOM,每次循环都触发一次重排。结果呢?页面加载要等好几秒。嗯,这里要注意——DOM操作是昂贵的

核心要点:每次DOM修改都可能触发重排(Reflow)和重绘(Repaint)。重排尤其昂贵,因为它会影响整个文档流。

那怎么优化?我个人的习惯是:

  • 批量操作:把多次DOM修改合并成一次
  • 离线操作:先在内存里改好,再一次性挂到DOM树上
  • 减少访问:缓存DOM引用,别每次都去查

举个例子,你写个列表渲染:

// ❌ 糟糕的做法
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `项目 ${i}`;
  document.getElementById('list').appendChild(li);
}

// ✅ 好的做法
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `项目 ${i}`;
  fragment.appendChild(li);
}
document.getElementById('list').appendChild(fragment);

看到区别了吗?第一种写法,每次循环都往DOM树上挂一个节点,触发了1000次重排。第二种写法,先在内存里攒好,最后一次挂上去,只触发一次重排。性能差距可不是一星半点。

3.2 DocumentFragment:轻量级的DOM容器

刚才那个例子就用到了DocumentFragment。这东西说白了就是一个虚拟的DOM容器,它不在真实的DOM树里,所以操作它不会触发重排。

我曾经在一个数据表格的项目里,需要动态插入几千行数据。一开始直接操作DOM,页面直接卡死。后来改用DocumentFragment,秒级完成。你说神奇不神奇?

小技巧:DocumentFragment不是唯一的方案。你也可以用字符串拼接,或者用innerHTML一次性赋值。但DocumentFragment的好处是,它保留了DOM节点的引用,方便后续操作。

它的用法很简单:

// 创建一个空的文档片段
const fragment = document.createDocumentFragment();

// 往里面添加节点
const div = document.createElement('div');
div.textContent = '我是片段里的内容';
fragment.appendChild(div);

// 一次性挂到DOM树上
document.body.appendChild(fragment);

注意,appendChild之后,fragment就空了。它就像个传送带,把节点送过去之后自己就清空了。嗯,这个设计挺巧妙的。

3.3 虚拟DOM:现代框架的基石

说到虚拟DOM,你可能马上想到React或者Vue。虚拟DOM到底是什么?说白了,就是一个用JavaScript对象模拟出来的DOM树。

你想想看,直接操作真实DOM太慢了,那咱们先在内存里用JS对象模拟一份,改的时候改这个对象,改完了再和真实DOM对比,只把变化的部分更新上去。这就是虚拟DOM的核心思想。

虚拟DOM的工作流程:

  1. 用JS对象描述DOM结构
  2. 数据变化时,生成新的虚拟DOM树
  3. 对比新旧两棵树的差异(Diff算法)
  4. 只把差异部分更新到真实DOM上

我刚开始学React的时候,觉得虚拟DOM多此一举。后来项目里有个实时更新的仪表盘,每秒要更新几十个数据点。如果用真实DOM,每次更新都得重新渲染整个组件,性能根本扛不住。虚拟DOM帮我只更新变化的部分,流畅多了。

下面我用SVG画个图,帮你理清虚拟DOM和真实DOM的关系:

虚拟DOM vs 真实DOM 工作流程 应用数据 状态变化 虚拟DOM树 JS对象描述 Diff算法 对比差异 真实DOM 只更新差异部分 页面渲染 用户看到的结果 核心思想:减少真实DOM操作,用JS计算替代浏览器重排 虚拟DOM层

3.4 增量渲染:别一次干完所有活

增量渲染这个概念,说白了就是「分批次干活」。你想想看,如果一次要渲染一万条数据,浏览器肯定卡死。那咱们一次只渲染一小部分,比如一次渲染20条,剩下的慢慢来。

我做过一个聊天应用,历史消息有几万条。如果一次性全部渲染,页面加载得等好几秒。后来用了增量渲染,每次只渲染最新的50条,用户滚动的时候再加载更多。体验好多了。

注意:增量渲染不是万能的。如果你的数据量特别大(比如几十万条),光增量渲染也不够,还得配合虚拟滚动(Virtual Scrolling)——只渲染可视区域内的节点。

增量渲染的实现思路大概是这样:

function renderIncrementally(data, batchSize = 20) {
  let index = 0;
  
  function renderBatch() {
    const fragment = document.createDocumentFragment();
    const end = Math.min(index + batchSize, data.length);
    
    for (let i = index; i < end; i++) {
      const item = document.createElement('div');
      item.textContent = data[i];
      fragment.appendChild(item);
    }
    
    container.appendChild(fragment);
    index = end;
    
    if (index < data.length) {
      // 用 requestAnimationFrame 保证不阻塞主线程
      requestAnimationFrame(renderBatch);
    }
  }
  
  renderBatch();
}

这里用了requestAnimationFrame,它会在浏览器下一次重绘之前执行,不会阻塞用户交互。嗯,这个API我特别喜欢,比setTimeout靠谱多了。

3.5 三者之间的关系

好,咱们捋一捋这三个概念的关系:

概念 核心作用 适用场景 性能提升
DocumentFragment 批量挂载,减少重排次数 一次性插入大量节点 中等
虚拟DOM 最小化真实DOM操作 频繁更新的复杂UI
增量渲染 分批次渲染,避免卡顿 大数据量列表

我个人觉得,这三个东西不是互斥的,而是可以组合使用的。比如在React里,虚拟DOM帮你减少了不必要的更新,但如果你要渲染一万条数据,还得配合增量渲染或者虚拟滚动。

我的建议:别一上来就想着用虚拟DOM。如果你的页面很简单,DOM操作不多,直接用原生DOM加DocumentFragment就够了。过度优化也是种浪费。

好了,这一章的内容就到这儿。记住一句话:DOM操作越少,性能越好。不管是DocumentFragment、虚拟DOM还是增量渲染,核心目标都是减少真实DOM的操作次数。你写代码的时候多想想这个,性能问题基本就能避免大半。


专注资料整理