Why Virtual DOM is slower

If you think it’s fast, this article might be what you were missing

Hajime Yamasaki Vukelic
7 min readApr 2, 2017

NOTES Mar 3, 2022: This article was written a long time ago. Since then not a lot has changed if you only look at the popular virtual DOM implementations. However, I’ve seen a few that get so close to Vanilla JS performance-wise that it would be hard to beat it unless you are really good at doing DOM manipulation by hand (e.g., Mikado). Please keep this in mind while reading the rest of the article. I would also like to note that I’ve switched fully to Vanilla JS since a few months ago, and I no longer believe that VDOM gives any developer ergonomics advantage compared to plain JavaScript.

You’ve been keeping up to date on the developments in the JavaScript community for the last couple of years, and you’ve at least heard of virtual DOM (React, Vue.js 2, Riot.js, Angular 2, and so on). They promise (or, rather, their fans promote) faster-rendering interfaces, especially updates, with less hassle. You rush into hacking together and app that uses virtual DOM, and it’s good. After a few months, your app is now growing in complexity, and you get updates that take a second or two from user interaction to on-screen update. “What gives? Wasn’t this thing supposed to be magically faster than jQuery?!” you think to yourself.

No, no, that’s not what it’s supposed to be!

While I agree that virtual DOM has provided us with much convenience, I will explain why I believe faster-rendering and faster-updating are, by definition, not true. There’s a price to be paid, with benefits that are not what most people seem to think or at least hope.

In order to follow this article, you will need to be familiar with DOM. Ideally, you would have at least played with the DOM API. If you’ve build serious stuff with just the DOM API, you probably don’t need this article, though, but I would still want you to read it and leave tips in the comments, for me and other readers.

Rendering and updates

Let’s take a look at the bird’s-eye view of how DOM node creation and update happens when you are doing it manually. This is important in order to understand how virtual DOM works, and what problems it solves.

When talking about JavaScript web apps, the changes to the user interface happen by DOM manipulation. This process has two phases:

  1. JS part: define the change in the JavaScript world
  2. DOM part: execute the change by using DOM API functions and properties

Performance is measured in terms of how fast the whole process is, but it is also important to know how fast each part is, in order to know what to optimize.

There are two ways to create and update parts of the DOM tree.

Working with strings is fast and easy, but it is not very fine-grained when it comes to updating. With strings, the JS part is the reason it is so fast. You can create a piece of HTML representing 5000 nodes in under a few milliseconds. Here’s an example:

const userList = document.getElementById("user-list");// JS part
const html = users.map(function (user) {
return `
<div id="${user.id}" class=”user”>
<h2 class="header">${user.firstName} ${user.lastName}</h2>
<p class="email"><a href=”mailto:${user.email}”>EMAIL</a></p>
<p class="avg-grade">Average grade: ${user.avgGrade}</p>
<p class="enrolled">Enrolled: ${user.enrolled}</p>
</div>
`
}).join("");
// DOM part
userList.innerHTML = html;

I mentioned there are limitations when using this method. Consider the following example:

const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.innerHTML = `<input class="search" type="text" value="bar">`;

Though it looks like the above is simple enough, it does not actually work. When we run the above code, the original <input> element is replaced instead of updated, and if, for example, user had the field focused, they will lose the focus.

An alternative way of creating and updating the DOM tree is to use DOM objects. This approach is quite verbose in terms of the code you have to write, and it is also much slower overall.

Let’s rewrite the user list example using this method:

const userList = document.getElementById("user-list");// JS part
const frag = document.createDocumentFragment();
users.forEach(function (user) {
const div = document.createElement("div");
div.id = user.id;
div.className = "user";
const header = document.createElement("h2");
h2.className = "header";
h2.appendChild(
document.createTextNode(`${user.firstName} ${user.lastName}`)
);
// ....
frag.appendChild(div);
});
// DOM part
userList.innerHTML = "";
userList.appendChild(frag);

This doesn’t look very nice, but it is nevertheless a valid way of creating DOM nodes. It also has an advantage of giving us the ability to mix this with a third-party library such as D3 to perform things that are not easy to do with HTML strings. The real advantage, though, is when performing granular updates to the existing tree:

const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.querySelector("input").value = "bar";

This time we combine the fast and convenient string HTML method for creating the initial UI, and then we use the DOM manipulation method to update the value property. Unlike the first time we did the this, the <input> is now not replaced, so it doesn’t cause UX glitches like in the first example.

Enter virtual DOM

Let’s go back to the first version of our input example:

const search = document.getElementById("search");
search.innerHTML = `<input class="search" type="text" value="foo">`;
// Change value to "bar"?
search.innerHTML = `<input class="search" type="text" value="bar">`;

If we parametrize the value part, it will look like this:

const search = document.getElementById("search");
const renderInput = function (value) {
search.innerHTML = `<input class="search" type="text" value="${value}">`;
};
renderInput("foo");
// Change value to "bar"?
renderInput("bar");

Well, the new renderInput() function sure looks cool and DRY, but we already know this is not the good approach.

What if we had some magic dust that would let us keep using something similar to this, but at the same time figure out what we are trying to do and do the right thing? Say, figure out that, the second time renderInput() is called, we are only updating the value attribute, so update only that attribute instead of rerendering the whole <input>?

Well, that magic dust is called virtual DOM. In order to perform the magic, virtual DOM implementation will first create a lightweight representation of the initial DOM tree you are creating, and then, on subsequent updates, it will generate a new representation, and compare it to the first one, in order to figure out what changed.

We said that the entire process of creating and updating the DOM tree has two phases. With virtual DOM, the DOM phase is supposedly going to be as efficient as possible, at the cost of the extra work done in the JS phase. This extra work results in the manual updates that you may write by hand, so another name for it would be overhead. Virtual DOM is, by definition, slower than carefully crafted manual updates, but it gives us a much more convenient API for creating UI.

Virtual DOM is, by definition, slower than carefully crafted manual updates.

Why some developers think Virtual DOM is faster

In the early days of virtual DOM (especially React), a myth was circulated that virtual DOM makes DOM updates fast. As we have seen in the previous sections, this is not technically feasible. DOM updates are what they are, and there is no magic that can make it faster: it has to be optimized in the browser’s native code.

You don’t see performance mentioned on the React’s home page. Check for yourself.

It is not about performance, but about developer convenience.

You still see benchmarks that compare various virtual DOM implementation, and some of the wording can mislead new developers into thinking that virtual DOM is the de facto standard today, and it’s not worth benchmarking it against other technologies. There are benchmarks that compare it to other technologies, though, such as the Aerotwist’s React + performance article, and it paints a more realistic picture of where virtual DOM is at in the grand scheme of things.

What do we get? Is it worth it?

Virtual DOM is ultimately a round-about way of performing DOM updates. However, it opens the door up to interesting architecture, such as treating views as function of state, or writing and composing view components. There are many good things virtual DOM brings to the table, even though insane levels of performance is not one of them. You could think of it as the difference between coding in Python or PHP vs coding in C. We get more developer ergonomy at the cost of performance. It’s a trade-off, in other words.

On the other hand, developer time lost is also a thing when it comes to some of the implementation. The part where virtual DOM tries to figure out what changes it needs to perform is implemented by humans, so it’s not always fool-proof. Sometimes you have to intervene. In some cases, it is not possible to intervene. For things that are absolutely performance-critical, it may not even be an option.

Measure your performance and decide according to hard data.

The bottom line is that the virtual DOM is just one of the tools you have at your disposal. Measure your performance and decide according to hard data. Data binding is still quite viable, and we have already seen you can do everything manually as well. It’s definitely not a be all end all thing, and there is therefore no need to get married to virtual DOM.

--

--

Hajime Yamasaki Vukelic

Helping build an inclusive and accessible web. Web developer and writer. Sometimes annoying, but mostly just looking to share knowledge.