Dom 批处理神器 DocumentFragment

#JavaScript性能优化实战#

DocumentFragment 到底是什么

  • 一个轻量级的 DOM 容器节点nodeType === 11),实现 Node/EventTarget不在文档树中,插入页面时自己不会被挂上去,而是**把它的所有子节点“搬家”**到目标容器,然后 Fragment 变空

  • 创建方式:document.createDocumentFragment()(兼容最好);在多数现代浏览器 new DocumentFragment() 也可用。

  • 与几个相近概念的区别

    • Element:真实页面元素,会参与样式/布局;Fragment 不会。
    • ShadowRoot:是真实附着在宿主上的“局部 DOM 树”;Fragment 只是临时容器。
    • template.content:是一个 DocumentFragment,处于“惰性 / inert”状态(脚本、图片不执行/加载,直到被移入文档)。
  • 事件与样式:Fragment 里构建时不触发渲染;你可以在节点上注册事件,但通常在挂载后再注册/委托更合理。


为什么要用 DocumentFragment

  1. 批量 DOM 更新更快、更稳
  • DOM 操作昂贵;把许多节点先拼到一个“离线容器”里,再一次性插入文档,可显著减少布局/绘制与样式计算的次数,降低抖动和闪烁。
  • 现代浏览器会对连续插入做一定合并,但一旦你在插入过程中读取布局信息(如 offsetWidth)、或与其它同步布局操作交错,仍会触发多次回流;Fragment 能把这批变更“打包”。
  1. 原子性与语义性
  • 先在内存中构建完整子树、校验完再一次性挂载:要么全部成功,要么不改动页面,避免中间状态被用户看到或被观察器捕获到多条松散记录。
  1. 与解析/模板很好地协作
  • <template>content 就是一个 DocumentFragmentRange.createContextualFragment() 也直接产出 DocumentFragment,适合把一段 HTML 字符串解析成节点,再一次性挂载。
  1. 减少观察器/事件干扰
  • 构建阶段不在文档树里,MutationObserver 不会被反复触发;最终插入通常只产生一次 childList 变更记录(addedNodes 为一组)。

小提醒:不是说 “不用 Fragment 就一定慢”。但在包含布局读取、复杂样式计算、或大量节点 的场景,用 Fragment 往往更可控、更稳。


如何高质量地用好 DocumentFragment

1) 大量插入:批量构建 + 一次挂载

const list = document.getElementById('list');
const frag = document.createDocumentFragment();

for (let i = 0; i < 5000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  frag.appendChild(li);         // 先拼在离线容器
}

list.appendChild(frag);         // 一次性挂载,frag 变空

与直接循环 append 的差异(含布局读取)

// ❌ 交替读写会迫使浏览器频繁同步布局
for (let i = 0; i < data.length; i++) {
  const li = document.createElement('li');
  li.textContent = data[i].name;
  list.appendChild(li);
  // 读取布局(强制回流)
  if (list.offsetHeight > 10000) break;
}

// ✅ 先在 Fragment 里构建,再统一挂载再读
const frag = document.createDocumentFragment();
for (const item of data) {
  const li = document.createElement('li');
  li.textContent = item.name;
  frag.appendChild(li);
}
list.appendChild(frag);
if (list.offsetHeight > 10000) { /* ... */ }

2) 用 <template> + Fragment 渲染列表(更可读)

<template id="row-tpl">
  <li class="row">
    <span class="name"></span>
    <span class="price"></span>
  </li>
</template>
<ul id="goods"></ul>
const tpl = document.getElementById('row-tpl');
const goods = document.getElementById('goods');
const frag = document.createDocumentFragment();

for (const item of items) {
  const node = tpl.content.firstElementChild.cloneNode(true); // 克隆模板项
  node.querySelector('.name').textContent  = item.name;
  node.querySelector('.price').textContent = `$${item.price}`;
  frag.appendChild(node);
}

goods.appendChild(frag);

由于 template.content 是 Fragment,里面的脚本不会执行、图片不加载,直到你把这些节点移入文档。


3) 把 HTML 字符串解析成节点:Range.createContextualFragment

比直接 innerHTML= 更灵活,能在特定上下文里解析(如表格单元、列表项等)。

const html = `
  <li class="msg"><strong>Info:</strong> Hello</li>
  <li class="msg"><strong>Info:</strong> World</li>
`;

const ul = document.querySelector('#messages');
const range = document.createRange();
range.selectNode(ul);  // 提供解析上下文(很重要)

const frag = range.createContextualFragment(html);
ul.appendChild(frag);

安全提示:createContextualFragment 一样会解析并执行潜在的危险内容(如 <img onerror=...>)。不信任的 HTML 必须先做 XSS 过滤/清理


4) 批量替换:replaceChildren + Fragment(原子更新)

function renderUsers(container, users) {
  const frag = document.createDocumentFragment();
  for (const u of users) {
    const li = document.createElement('li');
    li.textContent = `${u.id}${u.name}`;
    frag.appendChild(li);
  }
  container.replaceChildren(frag); // 等价于把 frag 的子节点整体换上去
}

replaceChildren 能保证一次性替换内容,避免部分渲染的中间态。


5) 批量剪切/搬移 DOM:Range.extractContents / cloneContents

  • extractContents() 返回一个 DocumentFragment,同时把这段从文档中移除
  • cloneContents() 则是只复制
// 把 <section id="a"> 到 <section id="b"> 之间的内容剪出来,插到 #target
const start = document.getElementById('a');
const end   = document.getElementById('b');

const range = document.createRange();
range.setStartAfter(start);
range.setEndBefore(end);

const frag = range.extractContents();    // 文档里这段被移除了
document.getElementById('target').appendChild(frag);

6) MutationObserver:减少噪声

观察大容器时,使用 Fragment 一次性插入能把多次节点添加“聚合”为一次变更记录(实现依浏览器,但一般是一条 childList,其中 addedNodes 包含全部新节点)。

const mo = new MutationObserver(records => {
  // 通常这里能拿到一条记录,里面含多个 addedNodes
});
mo.observe(list, { childList: true });

// 用 Fragment 批量插入
const frag = document.createDocumentFragment();
// ... append many children
list.appendChild(frag);

7) 常见陷阱与最佳实践

  • Fragment 插入后会被清空:以后还要复用?请先 frag.cloneNode(true)(但克隆会复制一份,注意成本),或每次重新构建。
  • 追加的是“搬家”不是“复制”:把已在文档中的节点 append 到 Fragment,会把它从原位置移走。
  • 事件绑定的时机:多数情况下,挂载后用事件委托更简单稳妥(绑定在容器上,利用冒泡)。
  • 脚本执行:通过 DOM API 创建并插入的 <script> 会按规则加载/执行;在 <template> 中是惰性的,移入文档后才会按正常规则生效。
  • 性能评估:用 Performance 面板或 performance.now()/console.time() 实测,不要只凭感觉。
console.time('frag');
const frag = document.createDocumentFragment();
for (let i = 0; i < 10000; i++) {
  const div = document.createElement('div');
  div.textContent = i;
  frag.appendChild(div);
}
container.appendChild(frag);
console.timeEnd('frag');

8) 什么时候我就不用 Fragment?

  • 少量节点、且过程中不读取布局,有时直接 append 也足够快、代码更简单。
  • 现代 API(如 element.append(node1, node2, ...))内部等效“批量”插入,写起来更短;不过显式使用 Fragment 能把“离线构建”的意图表达得更清楚,并便于做复杂拼装。

小结(可当作 Checklist)

  • 需要批量构建/插入或避免中间态 → 用 DocumentFragment
  • 解析 HTML → Range.createContextualFragment / <template>.content → Fragment。
  • 整块替换replaceChildren(frag)
  • 大段搬移Range.extractContents() / cloneContents()(返回 Fragment)。
  • 记住:插入后 Fragment 为空,节点是被“搬家”的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值