用好DocumentFragment,解决页面卡顿问题
很多前端开发者都遇到过这样的问题:页面需要展示大量数据时,滚动和操作会变得很卡。最近我就帮同事解决了一个类似的问题。他做的用户管理后台要显示上千条数据,每次打开页面都要等很久,滚动时一卡一卡的。
查看代码后,我发现他是这样写的:
for(let i = 0; i < data.length; i++) {
const row = createTableRow(data[i]);
table.appendChild(row);
}这段代码的问题很明显。每次循环都会向页面添加一行数据,浏览器每添加一次就要重新计算一次页面布局。数据量小的时候感觉不出来,但数据多了就会明显卡顿。
要解决这个问题,可以用DocumentFragment。
什么是DocumentFragment?
DocumentFragment可以理解为一个临时的容器。我们先把要添加的dom元素放到这个容器里,等所有元素都准备好后,一次性添加到页面中。这样做只需要一次页面重新计算,性能会好很多。
它的工作原理很简单。当我们直接操作页面上的DOM时,浏览器必须马上更新显示。但如果先在DocumentFragment里操作,浏览器就不会立即更新,因为DocumentFragment不在页面显示范围内。等所有操作完成,再把整个DocumentFragment的内容添加到页面,浏览器只需要更新一次。
看一个具体的例子:
// 创建临时容器
const fragment = document.createDocumentFragment();
// 在容器中添加元素,不会触发页面更新
for(let i = 0; i < 100; i++) {
const item = document.createElement('div');
item.textContent = `第 ${i} 条数据`;
fragment.appendChild(item);
}
// 一次性添加到页面,只触发一次页面更新
container.appendChild(fragment);这样写,无论添加100条还是1000条数据,页面都只更新一次。
DocumentFragment的几个特点
第一,它是一个独立的容器。在把它的内容添加到页面之前,你在里面做的任何操作都不会影响页面显示。
第二,它的内容是“转移”而不是“复制”。当你把DocumentFragment的内容添加到页面后,DocumentFragment自己就变空了。这意味着不会出现重复的元素,也不会浪费内存。
const fragment = document.createDocumentFragment();
const div = document.createElement('div');
fragment.appendChild(div);
console.log(fragment.childNodes.length); // 输出:1
document.body.appendChild(fragment);
console.log(fragment.childNodes.length); // 输出:0,内容已经转移到页面第三,在DocumentFragment中的元素不能触发事件。因为事件需要元素在页面中才能生效。只有把DocumentFragment的内容添加到页面后,上面绑定的事件才会起作用。
实际应用场景
渲染大量数据
这是最常用的场景。比如用户列表、商品列表、数据表格等。我们做过一个测试,渲染500个商品条目,原来需要800毫秒,使用DocumentFragment后只需要300毫秒。用户感觉页面响应快了很多。
创建复杂组件
比如要创建一个弹窗,里面包含遮罩层、内容区、关闭按钮等多个元素。可以先把所有元素在DocumentFragment中组装好,再一次性添加到页面。
function createModal(content) {
const fragment = document.createDocumentFragment();
// 创建遮罩层
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
// 创建弹窗内容
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerhtml = content;
// 组装
overlay.appendChild(modal);
fragment.appendChild(overlay);
// 一次性添加到页面
document.body.appendChild(fragment);
return overlay;
}移动多个元素
如果需要把一组元素从一个地方移到另一个地方,用DocumentFragment也很方便。
function moveItems(fromElement, toElement) {
const fragment = document.createDocumentFragment();
// 把元素逐个移到临时容器
while(fromElement.firstChild) {
fragment.appendChild(fromElement.firstChild);
}
// 一次性添加到新位置
toElement.appendChild(fragment);
}处理HTML字符串
有时候我们需要把HTML字符串转换成DOM元素。可以先用innerHTML解析,再用DocumentFragment处理。
function htmlToFragment(html) {
const fragment = document.createDocumentFragment();
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// 转移所有子元素
while(tempDiv.firstChild) {
fragment.appendChild(tempDiv.firstChild);
}
return fragment;
}需要注意的地方
虽然DocumentFragment很好用,但也不是所有情况都需要用它。
如果只是添加几个元素,直接操作DOM可能更简单明了。一般来说,当需要操作超过50个元素时,使用DocumentFragment的效果才比较明显。
现代前端框架如react、vue都使用了类似的思路。它们创建虚拟的DOM树,在内存中完成所有更新,然后一次性同步到真实DOM。了解DocumentFragment的原理,能帮助我们更好地理解这些框架的工作方式。
如何调试
在浏览器的开发者工具中,DocumentFragment显示为#document-fragment。虽然它不在页面DOM树中显示,但你可以在控制台查看它的内容。
实际效果对比
我们做了一个简单的性能测试:
添加10个元素:两种方法差异很小
添加100个元素:DocumentFragment快约30%
添加1000个元素:DocumentFragment快约60%
添加10000个元素:DocumentFragment快约70%
数据量越大,优势越明显。
什么时候使用
建议在以下情况考虑使用DocumentFragment:
需要渲染大量数据时
需要创建复杂的UI组件时
需要批量移动或修改多个元素时
对页面性能要求较高时
简单的使用原则
记住这个原则:如果操作会触发多次页面重新计算,就考虑用DocumentFragment合并成一次操作。
比如原来要添加100行表格数据,会触发100次页面更新。改成先用DocumentFragment收集所有行,再一次性添加,就只触发1次页面更新。
总结
DocumentFragment是一个简单但实用的工具。它能有效减少页面卡顿,提升用户体验。虽然现在有各种前端框架,但理解这些基础api的原理仍然很有价值。
下次当你需要操作大量DOM元素时,可以试试DocumentFragment。你会发现,有时候最简单的办法就是最有效的办法。
记住,好的性能优化往往来自对基础知识的深入理解。DocumentFragment就是这样一个值得掌握的基础知识。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!