While all of the the text and code here should (hopefully!) render for you, the live demos on this page use new es6 features that your browser does not support, because I wanted to write this post in modern JavaScript syntax and let readers edit and experiment with it. It's not you, it's me. Sorry :(

Virtual DOM is not an optimization

This is a blog post came from building a tiny dependency-free prototyping-oriented react-inspired javascript framework (because that's exactly what the world needs more of, right?).

React's Virtual DOM is sold as an optimization bolted onto a declarative UI to make it fast. I tried to build the slow version, and learned that Virtual DOM is not an optimization, but rather a foundational building block for declarative UIs on the web.

A Declarative UI

React is popular tool for building web application User Interfaces ("UIs" or "Views"). With React, developers write code that declaratively specifies what the UI should look like at any given point, without worrying about how to change the interface between states.

Here's a React component:

#react-demo-app

What's notable about React code is what it doesn't do. The above code says nothing about how to:

…so this is cool. React components just "re-render" themselves any time their state changes, and React takes care of updating the DOM for you.

React is fast because it uses a lightweight javascript representation of the DOM called a Virtual DOM to compute what has changed, and tries to touch the real DOM as little as possible when bringing it up to date. Changing the DOM is slow.

Attempting to achieve a declarative UI the slow way

The DOM may be slow, but what happens if we ignore that and just delete the whole DOM and make a new one from scratch instead of messing around with Virtual DOMs? Can we get a declarative UI that works like React but slower?

Here is my attempt:

#replace-dom-app

Seems to work!

So, React in 30 lines?

There's a big problem with this technique, already showing in the click-counting button example above. See it? It's more obvious if we try to render a text input:

#charcount-app

After you enter each character, the <input> you're typing in gets deleted. It's replaced with a new one, and your cursor is not focused on it. Same with the click-counting <button>: if you focus it and try to activate it multiple times with the space-bar, you'll find that you lose the button focus after each activation.

Refresh vs. Update Semantics

The broken demos above work correctly as specified, but they are unusable and feel broken because the semantics are wrong. Deleting the DOM and making a new one is like a page-refresh, so all transient UI state like :hover, :focus, and even scroll position, are lost along with the DOM. Just like in a page-refresh.

When the data changes, React conceptually hits the "refresh" button, and knows to only update the changed parts.

Why React from the React documentation

Re-rendering in React is not like a page refresh, because none of that transient state is lost. The DOM is just mutated.

Does this difference in semantics matter? Probably not. Even React's own documentation gets it wrong. But thanks for reading anyway :)

If not the Virtual DOM, what is the feature that makes React fast?

React components render a description of what the DOM should look like, called the Virtual DOM. What I've learned is that Virtual DOM is an implementation detail of creating a declarative UI library, not an optimization.

Reacts killer speed feature is its nifty algorithms that operate on the Virtual DOM in order to quickly compute a small but sufficient number of DOM mutations to perform on the actual page to bring it up to date. That's it!


Posted

Thanks for reading. Let me know what you think, I'm @uncyclephil on twitter.

If I get around to it or if someone is interested, I've got a post in mind about another tricky problem for declarative web UIs: correctly dealing with recursive state updates triggered by DOM updates. Sounds so exciting I know…

Postscript: The ugly Element.setAttribute special case: value

Most HTML attributes can be set on DOM elements via Element.setAttribute(attrName, value), but unfortunately, value is special, and can't be set in this way. It has to be assigned to the element directly instead (el.value = 'some text' instead of el.setAttribute('value', 'some text')). The example code in the post ignores this special case for clarity. As far as I can find, value is the only special attribute that can't be assigned via Element.setAttribute. Friends, the web platform!

Special-case handling for looping attrs might look like this:

Post-postscript: Can we get page-refresh semantics if we exhaustively declare even transient state?

The problem with the blow-the-dom-away approach above was that we lost transient state like :focus (that we might not want to track ourselves) every time we replaced the DOM. While writing this post, it occurred to me: what if did track and control all that extra state?

Starting with focus:

#controlled-focus-app

Hey now we are getting somewhere! This is a declarative UI with close to page-refresh semantics, no Virtual DOM, and it seems to work!

…well, almost. We're not capturing some more important transient state: the cursor position! This is more quickly noticeable on Firefox, which places the cursor at the beginning of the input on .focus(). Chrome puts it at the end, so you find the problem if you try to insert a word in the middle of some already-entered text. No reason we can't also manage and declare that state too though, right?

I'm not convinced that it's useful to go all the way with declarative UIs and manage every single bit of all the state in web applications.

So for the time being, I'm sticking to the simplest VirtualDOM implementation I can come up with.