Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is documentFragment no faster than repeated DOM access?

I was always under the impression that rather than touching the DOM repeatedly, for performance reasons, we should use a documentFragment to append multiple elements to, and then append the fragment into the document once, rather than just repeatedly appending new elements one-by-one into the DOM.

I've been trying to use Chrome's dev tools to profile these two approaches, using this test page:

<button id="addRows">Add rows</button>
<table id="myTable"></table>

Test 1 uses this code to append 50000 new rows to the table:

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);
    }
});

Clicking the button while recording in Chrome's Timeline tool results in the following output:

Repeated DOM interaction

Test 2 uses this code instead:

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);
});

This has a performance profile like this:

enter image description here

The second, supposedly much faster alternative to the first, actually takes longer to run! I've run these tests numerous times and sometimes the second approach is slightly faster, and sometimes, as these images show, the second approach is slightly slower, but not once has there been any significant difference between the two approaches.

What is happening here? Are browsers optimized so well now that this makes no difference anymore? Am I using the profiling tools incorrectly?

I'm on Windows 10, with Chrome 57.0.2987.133

like image 733
danwellman Avatar asked Apr 13 '17 20:04

danwellman


People also ask

What is DocumentFragment?

The DocumentFragment interface represents a minimal document object that has no parent. It is used as a lightweight version of Document that stores a segment of a document structure comprised of nodes just like a standard document.

What is fragmentation in Javascript?

DocumentFragment s are DOM Node objects which are never part of the main DOM tree. The usual use case is to create the document fragment, append elements to the document fragment and then append the document fragment to the DOM tree. In the DOM tree, the document fragment is replaced by all its children.

How do I use HTML fragments?

To create a new HTML Fragment:Click on the New HTML Fragment button at the top of the screen. In the Description box enter something descriptive so that you know what the HTML code is for or what it does. This is just for your reference and will not appear on your site. Paste your HTML code into the large HTML Code ...


1 Answers

Sorry... Due to the 30000 character limit in the answers and the length of the previous one i have to place another one as an extension to my previous answer.

I guess everybody's heard that direct DOM access is no good... Yes that's what i had always been told and so believed in the correctness of the virtual DOM approach. Though i hadn't quite understood the fact that while both the DOM and vDOM are represented on the memory, how come one is faster than the other? Actually streching my test further i have come to believe that the real bottle neck boils down to the JS engine performance if you update the DOM properly.

Now lets imagine the case of having 1000 divs to be updated repeatedly on background-color and height CSS properties.

If you do this directly on DOM elements all you have to do is to keep a nodeList of these elements and simply alter their style.backgroundColor and style.heigth properties.

If you do this by a document fragment you have the apparent benefit of not touching the DOM multiple times, instead first you have to

  1. clone the parent container of the 1000 div elements.
  2. access the nodeList containing the divs like parent.children
  3. perform necessary alterations on each div element,
  4. create a document fragment
  5. re-clone the previously cloned (step 1) parent container element and append it to a document fragment (or alternatively you may chose to clone the divs' container from the DOM if you need the fresh ones but this way or that way for each modification you have to clone them)
  6. append the document fragment to the parent of div container in the DOM and remove the old div container. Basically a Node.replaceChild operation.

In fact for this test we don't need a document fragment since we are not creating new nodes but just updating the already existing ones. So we can skip the step 4 and at step 5 we can directly use the cloned copy of our divs container as a source to our replaceChild operation.

How to update DOM properly..? Definitely asynchronously. So as for the previous example if you move the direct update portion to an asynchronous timeline like setTimeout(_ => renderDirect(),0) it will be the fastest among all. But then repeatedly updating the dome can be a little tricky.

One way to achieve this is to repedeatly feed a setTimeout(_ => renderDirect(),0) with our DOM updates like.

for (var i = 0; i < cnt; i++) setTimeout(_ => renderDirect(divs,width),0);

In the above case the performance of the JS engine is very material on the results. If our code is too light, than multiple cycles will stack up on a single DOM update and we will observe only a few of them. In this particular case, we got to see only like 9 of the 50 updates.

enter image description here

So delaying each turn further might be a good idea. So how about;

for (var i = 0; i < cnt; i++) setTimeout(_ => renderDirect(divs,width),i*17);

enter image description here

Well this is much better, I've got 22 of the 50 updates actually painted on my screen. So it happens to be, if the delay is chosen to be long enough you'll have all the frames painted. But how much long is a problem. Since if it's too long you have idle time for your rendering engine and it resembles slow DOM update. So for this particular test, it turns out to be something like 29-30ms ish... is the optimal value to observe 50 separate DOM updates of all 1000 divs in 1400 ms. Well at least on my desktop with Chrome. You may observe something entirely different depending on the hardware or the browser.

enter image description here

So the setTimeout resolution doesn't look very promising to me. We have to automate this job. Lucky us, we have the right tool for this job. rAF to help again. I have come up with a helper function to abuse the rAF (requestAnimationFrame). We will update the 1000 divs all at once, in one go, by directly accessing the DOM at the next available animation frame. And... while we are still in the asynchronous timeline we will request another animation frame from within the currently executing callback. So another rAF is called from the callback of the rAF recursively. I named this function looper

var looper = n => n && raf(_ => (renderDirect(divs, width),looper(--n)));

well it's a bit of an ES6 code. So let me translate it into classing JS.

function looper(n){
  if (n !== 0) {
    window.requestAnimationFrame(function(){
                                   renderDirect(divs,width);
                                   looper(n-1);
                                 });
  }
}

Now everything should be automated.. It seems pretty cool and done in 1385ms.

enter image description here

So since now we are a little more knowledgeable we may play with the code.

// Resets the divs
function resetLayout() {
  divs = document.querySelectorAll('div');
  speed.textContent = "Resetting Layout...";
  setTimeout(function() {
               each.call(divs, function(div) {
                                 div.style.height = '';
                                 div.backcgoundColor = '';
                               });
               speed.textContent = "";
             }, 16);
}
// print the result
function renderSpeed(ms) {
  speed.textContent = ms + 'ms';
}

function renderDirect(divs,width){
  each.call(divs, function(div) {
                    div.style.height = ~~(Math.random()*2*width+6) + 'px';
                    div.style.backgroundColor = '#' + Math.random().toString(16).substr(-6);
                  });

  // Render result
  renderSpeed(performance.now() - start);
}

function renderByVDOM(sct,prt,wdt){
  var //dFrag = document.createDocumentFragment();
      divs  = sct.children;
  each.call(divs, function(div) {
                    div.style.height = ~~(Math.random()*2*wdt+6) + 'px';
                    div.style.backgroundColor = '#' + Math.random().toString(16).substr(-6);
                  });
  //dFrag.appendChild(sct);
  prt.replaceChild(sct, divCompartment);
  // Render result
  renderSpeed(performance.now() - start);
}

var divs        = document.querySelectorAll('div'),
    width       = divs[1].clientWidth;
    raf         = window.requestAnimationFrame,
    each        = Array.prototype.forEach,
    isAfterVdom = false,
    start       = 0,
    cnt         = 50;

// Reset the Layout

reset.onclick = resetLayout;

// Direct DOM Access

direct.onclick = function() {
                   var looper = n => n && raf(_ => (renderDirect(divs, width),looper(--n)));
                   isAfterVdom && (divs = document.querySelectorAll('div'), isAfterVdom = false);
                   start = performance.now();
                   //for (var i = 0; i < cnt; i++) setTimeout(_ => renderDirect(divs,width),i*29);
                   looper(cnt);
                 };

// Update the vDOM and access DOM just once by rAF

vdom.onclick = function() {
                 var sectCl = divCompartment.cloneNode(true),
                     parent = divCompartment.parentNode,
                     looper = n => n && raf(_ => (renderByVDOM(sectCl.cloneNode(true), parent, width),looper(--n)));
                 
                 isAfterVdom = true;
                 start  = performance.now();
                 looper(cnt);
               };
html {
  font: 14px Helvetica, sans-serif;
  background: black;
  color: white;
}

* {
  box-sizing: border-box;
  margin-bottom: 1rem;
}

h1 {
  font-size: 2em;
  -webkit-hyphens: auto;
}

button {
  background-color: white;
}

div {
  display: inline-block;
  width: 5%;
  margin: 3px;
  background: white;
  border: solid 2px white;
  border-radius: 10px
}

section {
  overflow: hidden;
}

#speed {
  font-size: 2.4em;
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>repl.it</title>
    <link href="index.css" rel="stylesheet" type="text/css" />
    <script src="index.js" async></script>
  </head>
  <body>
  <h1>Updating 1000 DOM Nodes</h1>
  <section>
    <button id="reset">Reset Layout</button>
    <button id="direct">Update Directly</button>
    <button id="vdom">Update by vDOM</button>
    <section id="speed"></section>
    <section id="divCompartment">
      <div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div>
    </section>
  </section>
</body>
</html>

So the tests look like direct access through rAF is better than cloning and working on the clone and replacing the old one with it. Particularly when a huge DOM chunk is replaced it seems to me that the GC (Garbage Collect) task gets involved in the middle of the job and things get a little sticky. I am not sure how it can be eliminated. Your ideas are most welcome.

A Side Note: This test also shows that the current (Version 91.0.838.3) of the new chromium based Edge Browser is performing DOM renderings ~15% faster than the current (Version 89.0.4389.114) Chrome.

like image 178
Redu Avatar answered Oct 14 '22 21:10

Redu