我一直有这样的印象,出于性能原因,我们应该使用documentFragment
来追加多个元素,然后一次性将片段追加到文档中,而不是一个接一个地重复追加新元素到DOM中。
我一直在尝试使用Chrome的开发工具来分析这两种方法,使用这个测试页面:
<button id="addRows">Add rows</button>
<table id="myTable"></table>
测试1使用以下代码向表中追加50000个新行:
let addRows = document.getElementById('addRows');
addRows.addEventListener('click', function () {
for (let x = 0; x < 50000; x += 1) {
let table = document.getElementById('myTable'),
row = document.createElement('tr'),
cell = document.createElement('td'),
cell1 = cell.cloneNode(),
cell2 = cell.cloneNode(),
cell3 = cell.cloneNode();
cell1.textContent = 'A';
cell2.textContent = 'B';
cell3.textContent = 'C';
row.appendChild(cell1);
row.appendChild(cell2);
row.appendChild(cell3);
table.appendChild(row);
}
});
在Chrome的时间轴工具中录制时单击该按钮会产生以下输出:
测试2使用以下代码:
let addRows = document.getElementById('addRows');
addRows.addEventListener('click', function () {
let table = document.getElementById('myTable'),
cell = document.createElement('td'),
docFragment = document.createDocumentFragment();
for (let x = 0; x < 50000; x += 1) {
let row = document.createElement('tr'),
cell1 = cell.cloneNode(),
cell2 = cell.cloneNode(),
cell3 = cell.cloneNode();
cell1.textContent = 'A';
cell2.textContent = 'B';
cell3.textContent = 'C';
row.appendChild(cell1);
row.appendChild(cell2);
row.appendChild(cell3);
docFragment.appendChild(row);
}
table.appendChild(docFragment);
});
它的性能配置文件如下所示:
第二个,据说比第一个快得多的替代品,实际上需要更长的时间来运行!我已经多次运行这些测试,有时第二种方法稍微快一点,有时,如这些图像所示,第二种方法稍微慢一点,但两种方法之间没有任何显著差异。
这是怎么回事?浏览器是否已经优化得如此之好,以至于不再有任何区别?我是否错误地使用了分析工具?
我在Windows 10上,Chrome 57.0.2987.133
4条答案
按热度按时间xxe27gdn1#
实际上,你的测试代码只是插入节点,而不改变它们的内容或CSS,这实际上会迫使渲染引擎重新进行。
我准备了三个测试来证明这种巨大的差异。
1.简单的DOM修改导致布局垃圾
1.通过
window.requestAnimationFrame()
进行简单的DOM修改1.通过
document.createDocumentFragment()
修改虚拟DOM备注:
vlju58qv2#
对不起...由于答案的30000个字符限制和前一个的长度,我不得不放置另一个作为我前一个答案的扩展。
我想每个人都听说了直接访问DOM是不好的...是的,这就是我一直被告知的,因此我相信虚拟DOM方法的正确性。虽然我不太明白的事实是,虽然DOM和vDOM都表示在内存中,为什么一个比另一个快?实际上,在进一步扩展我的测试中,我开始相信真实的的瓶颈归结为JS引擎的性能,如果你正确地更新DOM。
现在让我们想象一下有1000个div在background-color和height CSS属性上重复更新的情况。
如果直接在DOM元素上执行此操作,则只需保留这些元素的nodeList,并简单地更改它们的
style.backgroundColor
和style.heigth
属性。如果您通过文档片段来实现这一点,那么您显然可以不用多次接触DOM,而是首先必须
1.克隆1000个
div
元素的父容器。1.访问包含div(如
parent.children
)的nodeList1.对每个
div
元素执行必要的改变,1.创建文档片段
1.重新克隆先前克隆的父容器元素(步骤1)并将其附加到文档片段(或者,如果需要新的容器,您可以选择从DOM克隆div的容器,但每次修改都必须以这种或那种方式克隆它们)
1.将文档片段附加到DOM中div容器的父级,并删除旧的div容器。基本上是
Node.replaceChild
操作。事实上,对于这个测试,我们不需要文档片段,因为我们不创建新节点,而只是更新已经存在的节点。因此,我们可以跳过步骤4,在步骤5中,我们可以直接使用divs容器的克隆副本作为replaceChild操作的源。
如何正确更新DOM?绝对异步。因此,对于前面的示例,如果您将直接更新部分移动到异步时间轴(如
setTimeout(_ => renderDirect(),0)
),则它将是所有时间线中最快的。但是,然后反复更新圆顶可以有点棘手。实现这一点的一种方法是重复地向
setTimeout(_ => renderDirect(),0)
提供我们的DOM更新,如。在上述情况下,JS引擎的性能对结果非常重要。如果我们的代码太轻,那么多个周期将堆叠在一个DOM更新上,我们将只观察其中的一小部分。在这个特定的情况下,我们只能看到50个更新中的9个。
因此,进一步推迟每一轮可能是一个好主意。那么怎么样;
好吧,这是好得多,我有22个实际上画在我的屏幕上的50个更新。所以它碰巧是,如果延迟被选择为足够长,你会有所有的帧画。但多久是个问题。因为如果它太长,你有空闲时间为您的渲染引擎,它类似于缓慢的DOM更新。因此,对于这个特定的测试,结果是29- 30 ms左右...是在1400 ms内观察所有1000个div的50个单独DOM更新的最佳值。至少在我的桌面上有Chrome。根据硬件或浏览器的不同,您可能会看到完全不同的东西。
所以
setTimeout
分辨率对我来说看起来不太有希望。我们必须使这项工作自动化。幸运的是,我们有合适的工具来完成这项工作。RAF再次提供帮助。我提出了一个帮助函数来滥用rAF(requestAnimationFrame)。我们将通过直接访问下一个可用动画帧的DOM,一次性更新1000个div。当我们仍然在异步时间轴中时,我们将从当前执行的回调中请求另一个动画帧。因此,从rAF的回调递归地调用另一个rAF。我将此函数命名为looper
这是一个ES6代码。所以让我把它翻译成类JS。
现在一切都应该自动化。它看起来很酷,在1385 ms内完成。
所以现在我们有了更多的知识,我们可以玩代码。
因此,测试看起来像是通过
rAF
直接访问比克隆和处理克隆并用它替换旧的要好。特别是当一个巨大的DOM块被替换时,在我看来,GC(垃圾收集)任务会被卷入到工作的中间,事情会变得有点棘手。我不知道如何消除它。你的想法是最受欢迎的。**A Side Note:**此测试还显示,当前(版本91.0.838.3)的新Chrome Edge Browser执行DOM渲染的速度比当前(版本89.0.4389.114)Chrome快约15%。
7rtdyuoh3#
我不知道如何准确地解释你的分析器结果,但通过benchmark.js测试显示第二个选项在Chrome中更快:https://jsbench.me/awj1gwhk9i/1
FF中也更快(尽管差异较小)
zfciruhq4#
这个lifehack(最后一个按钮,名为“隐藏/显示”)-https://jsfiddle.net/qnxfhtyL/怎么样?
它基于上述演示,似乎是最简单和最快的-至少对于这个特定的测试。
诀窍是只隐藏html块(divCompary.style.display = 'non'),然后根据需要处理它,然后再次显示出来(divCompary.style.display = 'block')。浏览器将不必重画它1000倍,而它的隐藏。隐藏和展示仍然不被注意。下面是整个代码: