Course: JavaScript

Progress (0%)

  1. Foundation

  2. Numbers

  3. Strings

  4. Conditions

  5. Loops

  6. Arrays

  7. Functions

  8. Objects

  9. Exceptions

  10. HTML DOM

  11. CSSOM

  12. Events

  13. Drag and Drop

  14. opt Touch Events

  15. Misc

  16. Project: Analog Clock

HTML DOM - Document Fragments

Chapter 49 17 mins

Learning outcomes:

  1. What are document fragments
  2. The DocumentFragment interface
  3. Purpose of document fragments
  4. Document fragments might not always be faster!
  5. Using document fragments

Introduction

In the previous chapter HTML DOM — Selecting Elements, we got to know about a bunch of ways to select elements in an HTML document — an extremely important concept. Now, it's the high time that we learn about yet another important DOM concept, which is that of document fragments.

Document fragments in JavaScript are a handy feature to use when working with multiple node insertions at a time in the DOM. They allow us to group various nodes into one single unit and then operate that unit as if it were a single node.

In this chapter, we'll learn what exactly is a document fragment, the DocumentFragment interface, methods available on it, when and why could we ever feel the need to use it in our code.

Let's begin.

What are document fragments?

In simple words:

A document fragment is merely a lightweight version of a document.

If were to be extremely precise, then a document fragment is an extremely lightweight, not just lightweight, version of a document.

The sole purpose of a document fragment is to store a set of nodes temporarily before we dump them in some part of the main DOM tree, associated with a document.

On itself, a document fragment is of NO use. Seriously. A document fragment is always used in relation to a larger DOM tree where its contained nodes will later on be inserted.

A document fragment can be thought of as a means of grouping a set of nodes into one single node.

In the DOM API, the DocumentFragment interface is used to represent document fragments. It inherits from the Node interface, which confirms the fact that a document fragment groups a set of nodes into a single node (i.e. it's a Node itself).

The usage of document fragments is popular because of the performance improvement they offer to us when inserting multiple nodes into the DOM tree. We'll learn more about this shortly below.

First, let's explore the properties and methods available on the DocumentFragment interface to help us in adding nodes to it:

MethodPurpose
append()Works the same way as the append() method available on the Element interface. (Note that it's not the same method.)
prepend()Works the same way as the prepend() method available on the Element interface. (Note that it's not the same method.)
insertBefore()The insertBefore() method of the Node interface.
appendChild()The appendChild() method of the Node interface.
replaceChild()The replaceChild() method of the Node interface.
removeChild()The removeChild() method of the Node interface.
As stated earlier, DocumentFragment inherits from Node; hence, all Node properties and methods are available on DocumentFragment instances as well.

Purpose of document fragments

Document fragments are popular more than just as a means of grouping nodes. In this section, we'll explore the purpose of using document fragments.

Back in the day, document fragments were mainly used as an optimization tactic to improve the performance of the browser engine while inserting nodes into the DOM tree. And it won't be wrong to say that even today, this is one of their core utilities, or perhaps, their core utility.

So how do document fragments improve the performance of the browser engine?

Well, let's understand this with the help of an example.

Consider the following code:

<ol></ol>
var languages = ['Python', 'Perl', 'Java', 'Ruby', 'Erlang', 'C++', 'C', 'TypeScript', 'Rust', 'Lisp'];
var orderedListElement = document.querySelector('ol');

var listItemElement;
for (var i = 0, len = languages.length; i < len; i++) {
   listItemElement = document.createElement('li');
   listItemElement.textContent = languages[i];
   orderedListElement.appendChild(listItemElement);
}

It's fairly simple to understand. We are adding a couple of <li> elements into the <ol> element shown, based on the items in the languages array, each one separately using the appendChild() method.

Now, here comes the interesting part...

Whenever a mutation happens in the DOM — which is just a fancy word for 'change' — the browser is tasked with performing a great deal of calculations regarding the styles of multiple elements on the page.

But why?

Simply to prepare for the next graphical rendering of the page on the screen.

The process of performing these computations is collectively referred to as page reflow. The process of actually rendering the graphics on the screen with the help of the page reflow is known as page repaint.

In the upcoming JavaScript Dimensions unit, we'll learn about various properties and methods available in the DOM API that enable us to retrieve the widths and heights of elements, and even their distances from given reference points (such as other elements or the viewport).

After every single DOM mutation, the browser has to rerun the page reflow algorithm so that these properties and methods return the latest, appropriate values.

Coming back to the code above, in the for loop, as we call appendChild(), which is a DOM mutation action, we incur the cost of the page reflow algorithm. Cumutatively, this means that the page reflow algorithm is triggered 10 times (as there are a total of 10 items in the languages array).

And that's the problem right there.

Continuously mutating the DOM via multiple calls to such mutation methods as appendChild(), insertBefore(), replaceChild(), etc, causes the page reflow algorithm to be performed by the browser again and again.

And, as mentioned before, this can be an inefficient action in terms of the performance of the underlying web page.

A much better approach is to add all the nodes to a document fragment and then, in the end, insert the fragment right where we want to insert those nodes. In effect, this would produce the exact same output as inserting each of the nodes individually, yet it would incur the cost of only one single page reflow.

This is a clear-cut advantage of using document fragments.

Document fragments might not always be faster!

Browser engines have become extremely sophisticated these days. When document fragments were first introduced into the DOM API, this wasn't the case, but now it sure is.

Nowadays, engines can make extremely precise and well-educated guesses about what's happening, or will happen, in a given piece of code, and likewise make a plethora of various optimizations to it, either before it's executed or while it's executing.

As stated in the Performance section on the DocumentFragment Interface at MDN's website:

The performance benefit of DocumentFragment is often overstated. In fact, in some engines, using a DocumentFragment is slower than appending to the document in a loop as demonstrated in this benchmark. However, the difference between these examples is so marginal that it's better to optimize for readability than performance.

If we check out the benchmark linked above, we see some surprising results.

That is, the code where multiple nodes are added individually to the DOM tree (using append()) is faster than the one where we employ a document fragment to do the same thing.

But how on Earth can this happen? Didn't we just learn above that document fragments are more performant than inserting multiple nodes individually into the DOM?

Well, it's because what we often see in a given piece of JavaScript code is not what actually gets executed. As we said previously, engines are extremely intelligent these days and apply numerous kinds of optimizations to a given piece of code by analyzing what's happening, or will happen, in it.

A similar thing happens with the codes in the benchmark above.

Let's understand it quickly...

While executing the code where multiple nodes are added individually to the DOM, the engine realizes the fact that there is no need of performing the page reflow algorithm while running the loop and thus keeps itself from firing the algorithm again and again, after each node insertion. It's only in the end that it performs the reflow.

Comparing this with the code where we use a document fragment, there we first add all the desired nodes to the fragment and then dump them from the fragment into the DOM tree. This latter step, i.e dumping nodes from the fragment into the DOM, costs a little bit more time than the code above.

Note that if the browser engine performed a reflow after literally every single DOM mutation, regardless of whether or not it would really be required, then the first code (where multiple nodes are inserted individually into the DOM) would undoubtedly have been the loser in the benchmark.

But thanks to engine optimizations, we really never know which piece of code would outperform the other, whereby we expect a different result at the first glance.

So now, what should we make of all of this discussion?

Well, we should continue to use document fragments as we use them right now, but keep the fact in mind that they won't always be the faster choice.

Sometimes, depending on the optimizations produced by JavaScript engines, document fragments might have the opposite effect on performance.

Note that this does NOT mean that we should benchmark each of the bits of our code and then use the one that's faster. NO!

We should instead always run after code readability and make obviously-performant code choices (i.e. the ones that seem to be efficient).

Premature optimizations — such as replacing a code utilizing document fragments for inserting nodes with a code individually inserting those nodes, one-by-one — shouldn't concern us at all. We must always write code that seems performant.

This means that we must favor the usage of document fragments when wanting to add multiple nodes in the DOM tree. Sometimes, such a code might be slower, but the performance change would be absolutely negligible at the scale of a web page.

Phew! This was a lot, wasn't it?

Using document fragments

It's time to consider some examples.

Let's rewrite the code that we saw above, where we were adding <li> nodes inside an <ol> element, this time using a document fragment.

Consider the following code:

<ol></ol>
var languages = ['Python', 'Perl', 'Java', 'Ruby', 'Erlang', 'C++', 'C', 'TypeScript', 'Rust', 'Lisp'];

var fragment = document.createDocumentFragment(); var listItemElement; for (var i = 0, len = languages.length; i < len; i++) { listItemElement = document.createElement('li'); listItemElement.textContent = languages[i];
fragment.appendChild(listItemElement); } var orderedListElement = document.querySelector('ol');
orderedListElement.appendChild(fragment);
  • First, we create a document fragment by invoking the createDocumentFragment() method of the document object.
  • Next, we step into a for loop and create an <li> node in each iteration, for each item of the languages array. This node is inserted into the fragment by calling the appendChild() method of the fragment.
  • After the loop completes, the fragment is inserted inside <ol>. This causes all the nodes inside the fragment to be moved into the <ol> element, leaving the document fragment itself empty.

Here's a live example:

Live Example

Perhaps the most important thing to note here is that upon inserting the document fragment into the DOM tree, it gets emptied.

Why does this happen is quite intuitive to reason about, as detailed below:

Why is a document fragment emptied upon insertion?

Let's try to think about this very naturally, based on intuition.

Imagine that when a document fragment was inserted into the DOM, all the nodes inside it were cloned and then inserted into the DOM, keeping the fragment itself filled up with the original nodes.

Quite obviously, without further ado, this would've been an extremely inefficient approach. Cloning a node is a costly operation, and multiplying this effect for numerous nodes can be slightly, if not very, inefficient. So there is no point of cloning.

Now imagine that when a document fragment was inserted into the DOM, all the nodes inside it were moved into the DOM but weren't removed from the fragment.

As we can reason again, this would've been problematic as well. The exact same nodes, in terms of memory, would be present in the DOM as well as in the fragment. In the DOM, they would have a parent node, but in the fragment, this won't, and can't ever, be the case.

In the DOM API, a node can be part of only one single tree at a moment, whether that's the tree of a fragment or of the main document. Moving nodes from a fragment into the DOM without removing them from the fragment would've simply been a conflict to this notion, and even in general, a senseless idea.

Hence, it's the case that when a document fragment is inserted into the DOM, all the nodes within it are moved into the DOM, leaving the fragment itself empty.

Let's consider another example.

As a document fragment is emptied upon insertion into the DOM, we might think that it could be reused for subsequent applications of another fragment. Guess what, that's actually the case.

In the code below, we extend the previous code to add an <h1> and <h4> element before <ol> with the help of the same document fragment instance that we used to fill up the <ol> element:

<ol></ol>
var languages = ['Python', 'Perl', 'Java', 'Ruby', 'Erlang', 'C++', 'C', 'TypeScript', 'Rust', 'Lisp'];

var fragment = document.createDocumentFragment();

var listItemElement;
for (var i = 0, len = languages.length; i < len; i++) {
   listItemElement = document.createElement('li');
   listItemElement.textContent = languages[i];
   fragment.appendChild(listItemElement);
}

var orderedListElement = document.querySelector('ol');
orderedListElement.appendChild(fragment);

// Add an <h1> and <h4> element before <ol>.
var h1Element = document.createElement('h1');
h1Element.textContent = 'A heading';
fragment.appendChild(h1Element);
var h4Element = document.createElement('h4');
h4Element.textContent = 'A sub-heading';
fragment.appendChild(h4Element);
orderedListElement.parentNode.insertBefore(fragment, orderedListElement);

Live Example

As can be seen in the link above, the elements <h1> and <h4> do get added to the document via fragment, even though it had been used previously as well.

This confirms the fact that a document fragment can be created once and used again and again.