My previous post described how to create a very basic web application following the principles of functional programming. That’s fine, but I bet you’re not building basic apps. How to scale this approach? This (and the following) post will present some techniques you could use to solve common problems encountered when creating more complex applications.
Our little framework relies on
view function which translates state into DOM tree. The function is invoked on every state update. This means that on every state change we need to re-create the whole DOM tree and have it re-rendered by the browser. In a complex application with multiple actions and huge DOM tree this could have a huge impact on performance. Below you can find a screenshot illustrating the problem. The whole
div is updated even though clicking Complete should only affect two table rows.
Basically, we’d like to limit the amount of unnecessary DOM-related work. On the other hand, we want our code to stay declarative and functional so we have to avoid direct, imperative DOM manipulation.
Virtual DOM is the answer to our problems! It is a clever technique where instead of creating actual DOM objects you operate on virtual DOM elements. Operations on virtual nodes are much faster than on actual nodes since there is no browser API involved. Obviously, at some point we need to update the actual DOM tree. Here is how we will do this:
viewfunction will return a virtual DOM tree
- on every state update (every
appinvocation) we will compare the result of the
viewcall with the previous tree
- the comparison results in a set of
patchesthat represent minimal changes to the DOM
patchescan be applied to the actual DOM tree; only relevant parts of the DOM are updated, not the whole tree
Below you can see the difference after enhancing the application with virtual DOM. Note that only relevant parts of the DOM are highlighted.
Let’s rework our application to take advantage of virtual DOM.
The first step is to adjust the
view function to return virtual nodes instead of real DOM nodes. We are not going to implement the virtual DOM mechanism itself. It’s a highly non-trivial task and not in the scope of this article. Instead, let’s use one of existing virtual DOM libraries. Our library of choice is simply called virtual-dom. The best thing about it is that it’s compatible with
hyperscript-helpers. Remember how we wrapped
hyperscript-helpers so that we were able to use functions such as
table, etc.? This extra level of indirection will prove enormously useful now. Our
view function will continue using these functions. However, they will proxy to
virtual-dom instead of
hyperscript resulting in virtual DOM nodes instead of real DOM nodes.
import h from 'virtual-dom/h';
That’s it! There are no more changes to
The next (and last) step is to adjust the
app function. It’s going to get a bit more complex. Before, all we had to do was to replace the old DOM tree with the new tree. However,
view returns a virtual tree now. We need to compare the new tree and with old one using
diff function provided by the library. The comparison will return a set of
patches which can be later applied on the actual DOM tree. The above procedure describes what happens on state update. However, we need an initial DOM tree to begin with! We can get one from the initial virtual tree by calling
createElement, also provided by the library. Below you can find the update code.
import diff from 'virtual-dom/diff';
app function accepts a new parameter called
previousView. We need it to be able to compare updated virtual DOM with the previous version. When
null, it means that
app is called for the first time (with
initialState) and that there is no real DOM tree in place yet. Therefore, we call
createElement and append the result to
previousView is not empty, we should compare it with the new virtual tree (
updatedView) and apply patches on
rootNode‘s first child (because we initially attached the whole tree to
rootNode). Obviously, this part is not functional code. Patching the DOM is an imperative operation with side effects. However, this part of the code wasn’t pure in the first place. What’s important is that we’ve managed to preserve the purity of the rest of the code.
The concept of virtual DOM is instrumental in React framework. This is actually what made React famous for its performance. By the way, if you are familiar with React, you might have noticed similarities between our application and the framework. Indeed, our
view function is nothing less than a React functional component! It’s interesting to see how other frameworks deal with limiting the amount of DOM operations while maintaining declarativeness. For example, Angular takes a different approach based on change detection. You can read more about it in one of my posts. This comparison between those two mechanisms seems very interesting to me and I asked a question on Reddit specifically about it. I’ve got an amazing reply from Rob Wolmard which details the pros and cons of both approaches:
The tradeoff (and part of the philosophy Angular is built around) is that templating allows Angular to deeply understand a template, and generate highly optimized code, in both pure CPU cycles, but low memory consumption and garbage collection. The flexibility of being able to return whatever from a JSX-style
render()function means the framework has to be able to handle whatever, and each time a new virtual DOM representation is created, it can consume a fair amount of memory - especially important on low end devices.
In this post you’ve learned what virtual DOM is and how frameworks can use it to optimize performance while remaining declarative. I believe that this example nicely illustrates how trying to build your own framework can push you to learn new things and understand how existing frameworks work underneath.