Knowledge for the World

Learn the slow (and fast) way to append elements to the DOM

jQuery is a powerful tool for front-end developers, but it does not alleviate the responsibility of ensuring your code is efficient. One common pitfall for developers is the method used to append elements to the DOM. This guide will examine the handful of different ways of doing this and point you to the most efficient way.

This subject matters most at a large scale. So for this guide, we will assume that our task is to append 10,000 divs to the body of a webpage.

In these interests [?]
1

A naive approach

Early on I would've done this:

for (var i=0; i<10000; i++) {
  $("BODY").append($("<div />").addClass("test-div"));
}

This code produced an average running time of 779.463ms. This means it took over three quarters of a second to append these elements to the DOM.

So what is the problem here?

Well, there are a few - too many reflows and too many jQuery objects. We'll address the latter in the next step, so let's look at reflow here.

Simply put, reflow is when the browser needs to process and draw the webpage, and it is one of the most expensive browser processes. One of the easiest ways to improve the performance of your application is to minimize the number of reflows, and that's what we're going to do.

Many different operations can cause a reflow, and it isn't the same for every browser. But rest assured, appending an element to the DOM will cause a reflow. So the obvious problem with our code snippet is that we are generating 10,000 reflows. Some browsers may handle this intelligently and reduce some of the cost, but we can do better.

2

Fix the reflow problem

Let's fix our reflow problem by creating a node that has not yet been added to the DOM. Then we will append our divs to this node. And finally, we will append this single node to the DOM which should trigger a single reflow.

var $c = $("<div />").addClass("container");
for (var i=0; i<10000; i++) {
    $c.append($("<div />").addClass("test-div"));
}
$("BODY").append($c);

This is a little bit better but still not great. Our running time is now 432.524ms. This is still pretty bad, and there is a lot we can do to improve.

The problem here is that we're still creating too many jQuery objects. Even though we've solved the reflow problem, there is a lot of overhead to create a jQuery object. Of course, for a single object the convenience of jQuery far outweighs the minimal performance hit. But if we're dealing with 10,000 elements the inefficiency is more than noticeable.

Let's see what happens when we skip using jQuery altogether and write this in plain JavaScript.

var c = document.createDocumentFragment();
for (var i=0; i<10000; i++) {
    var e = document.createElement("div");
    e.className = "test-div";
    c.appendChild(e);
}
document.body.appendChild(c);

Incredibly, this change reduced our total running time from 432.524ms to 16.237ms. And this makes sense because it actually takes jQuery some time to create the object. Let's see just how long it takes to create a single jQuery object vs. creating the element using vanilla JavaScript.

console.time("jquery div");
var jqDiv = $("<div />");
console.timeEnd("jquery div");
// jquery div: 0.272ms

console.time("js div");
var jsDiv = document.createElement("div");
console.timeEnd("js div");
// js div: 0.006ms

This is a significant difference - 0.006ms vs. 0.272ms. The point of this comparison isn't to say that jQuery is bad but rather that as a developer you should know when to use it and when not to. jQuery provides a lot of functionality that isn't necessary for our purpose so it doesn't make sense to create 10,000 jQuery objects at such a high cost.

3

Using strings instead of nodes

We don't actually have to create 10,000 nodes. Instead we can see what happens when we use one long string of HTML.

var s = "";
for (var i=0; i<10000; i++) {
    s += "<div class=\"test-div\"></div>";
}
$("BODY").append(s);

At 69.874ms this performs much better than our original, but not quite as well as the pure JavaScript version. However, there is one small improvement we can make. String concatenation is expensive, especially at this scale. Let's use an array of strings and join them in the end.

var a = [];
for (var i=0; i<10000; i++) {
    a.push("<div class=\"test-div\"></div>");
}
$("BODY").append(a.join(""));

This runs in 63.885ms. This probably isn't enough of an improvement to stress out about, but since it's faster we'll keep it.

Let's measure the cost of using jQuery to append the string. As we learned in the previous step, using pure JavaScript may be faster.

var a = [];
for (var i=0; i<10000; i++) {
    a.push("<div class=\"test-div\"></div>");
}
document.body.innerHTML = a.join("");

The main difference here is that we're not using jQuery.append. Instead we're setting the innerHTML attribute on the body to our string of HTML. This runs in 22.908ms so the improvement is obvious. But we're still not as fast as the pure JavaScript version from the last step.

4

Conclusion

In summary, the fastest code was the pure JavaScript version:

var c = document.createDocumentFragment();
for (var i=0; i<10000; i++) {
    var e = document.createElement("div");
    e.className = "test-div";
    c.appendChild(e);
}
document.body.appendChild(c);

Based on these experiments there are two major takeaways.

First, be aware of which operations will cause a reflow. This is an expensive process, and it should be reduced as much as possible.

Second, be aware of the cost of using jQuery. For operations at a low scale, the convenience of jQuery will often outweigh the performance cost. But at a large scale, jQuery should be avoided unless it is actually needed.