What is Virtual DOM and What Problem Does it Solve?

Aug 22, 2020


This post aims to offer a jargon-free answer to the following questions:

  • What is Virtual DOM?
  • Why do some frameworks (like React & Vue) use Virtual DOM?
  • What problem does it solve?
  • Does it make rendering to the UI faster or slower?

Let's talk about DOM

Before we talk about the Virtual DOM, let's first understand the DOM & how the browser renders DOM updates to the user's screen:

What is DOM?

DOM (Document Object Model) is a tree representation of the HTML rendered by the browser. Along with this tree representation, DOM provides an interface to modify anything in it's tree structure. So, to modify something rendered on a page, the JavaScript needs to modify it's respective element in the DOM tree. And this can be done via DOM's interface.

Below is a simple HTML. Along side is the DOM representation of the HTML when rendered in the browser:

<html lang="en"> 
  <head> 
    <title>Example Page</title> 
  </head> 
  <body> 
        <p id="content">Some paragraph text</p>
        <img src="/image-url" width="100px" height="100px" />
  </body> 
</html> 
Document
  • Root Element <html>
    • Element <head>
      • Element <title>
        • text "Example Page"
    • Element <body>
      • Element <p>
        • attribute-id "content"
        • text "Some paragraph text"
      • Element <img>
        • attribute-src "/image-url"
        • attribute-width "100px"
        • attribute-height "100px"

The JavaScript statement document.getElementById('content').innerText += "A lot more paragraph text"; will change the corresponding object in the above DOM tree.

How are DOM updates rendered on the screen?

The browser watches the DOM tree for any changes. Whenever we change anything in the DOM tree, the browser syncs the UI accordingly. To do so:

  • The browser combines the DOM tree with page's styling.
  • It then considers the user's browser window dimensions to construct page's layout.
  • Based on the DOM or style change, it determines elements whose layout is affected.
  • Finally, it repaints the affected elements on the screen.

In the example in the previous section, changing the <p> DOM element (the one with id="content") may require repainting that element and the image underneath it.

The browser renders DOM updates to the UI. This can sometimes consume a lot of CPU & memory because changing the layout of one element may affect the layout of many other elements on the screen.

How DOM updates can lead to unresponsive pages & janky experiences?

Real world web pages often contain hundreds (or thousands) of elements. Updating a single element in the DOM tree may lead to a layout change & repaint for many other elements. And, multiple repaint cycles of this kind can mean a lot of work for the browser. This can result into unresponsive pages & janky experiences - more so, on mobile devices.

This can happen more frequently than we would imagine as we can trigger a stream of expensive repaints on events like user-scroll or key-press. In this way, seeming simple DOM updates can lead us to a landmine of unresponsive pages or janky experiences.

Seemingly simple DOM updates, when triggered repeatedly via events like user-scroll or key-press, can lead to unresponsive pages or janky experiences.

Let's talk about Declarative Programming

Let's keep all our DOM updates understanding aside for a moment. And, let's try to understand what is declarative programming in the React world. And, what it has got to do with all DOM updates stuff we just talked.

Declarative Programming with React

Modern web-apps are a lot more interactive & feature-rich than the primitive ones. Think of a collaborative browser-based editor or a web based chess game. These apps allow users to drag-and-drop items, maintain state of a lot of UI things, automatically sync stuff in the background, etc. As we add more features, handling UI for such web apps in plain JavaScript can become error-prone & difficult to maintain.

This is where React's declarative programming helps. Let's try to understand how:

With declarative programming, we just state what needs to be done without worrying about how it is done. Think SQL or CSS (which are both declarative programming languages). A CSS statement background-color:white; tells the browser what background-color should be. It doesn't tell the browser how to achieve the specified styling. Declarative programming with React allows us to do this for what we want to display on the UI.

Let's understand this better with a simple React example below:

In the example above, we just specify that the heading displays this.state.message. And then, anytime this.state.message changes, React will automatically update the heading in the UI. So, we achieve interactivity by changing the state of our components & let React handle updating the DOM accordingly. We just specify the what and not the how.

How is the declarative React code easier to manage?

The simple example above doesn't explain how declarative React code can be less error-prone & easier to maintain. But, let's imagine a calendar web app where timeslot cells can have many different values. And these values can be changed via various user actions. Add to this, shared calendars, background-syncing and similar functionalities. The code to manage a large number of user-interactions & on-screen elements can quickly become error-prone.

React's declarative programming requires us to ensure our components state is as expected and takes care of the rest. This makes our code more predictable & easier to debug.

By helping make our UI code predictable & easier to debug, declarative programming makes it easier to maintain & manage our UI code.

But, what has declarative programming got to do with DOM updates?

Declarative programming doesn't require us to worry about how the DOM elements are updated. While this makes our UI code simpler, it increases the probability of us (inadvertently) writing code that leads to frequent DOM updates.

A common example of this is React triggering the render for children components when the state for a parent component is updated. With the large number of components to handle, this happens commonly. If parent & all it's children component elements on the DOM are re-rendered on every such state change, we will find ourselves stepping upon the landmine of janky experience we described earlier.

This is where the Virtual DOM comes in handy.

Declarative UI programming allows us to remain unaware how the DOM elements on our UI are updated. But, this increases the likelyhood of us writing code that leads to frequent expensive re-renders.

Virtual DOM

Now that we understand declarative programming & how it affects rendering DOM updates, let's talk about the Virtual DOM:

What is Virtual DOM?

A virtual DOM is an in-memory object maintained as a copy of the DOM tree. For Virtual DOM based frameworks (like React or Vue), we render our UI changes to the virtual DOM (and not directly to the DOM). The framework then syncs the virtual DOM with the real DOM. The real DOM changes are then picked by the browser for repainting.

What purpose does Virtual DOM serve?

By maintaining a separate copy of the DOM tree, React can control when and what changes are passed on to the DOM for rendering. And, this offers it a chance to optimize these changes to minimize repaints. In other words, React doesn't directly pass the UI changes from the virtual DOM to the real DOM. Instead, it applies it's own diffing logic to minimize the re-renders.

So, when something about a component (or it's parent) changes, React applies it's diffing logic to minimize re-renders. So, central to React's virtual DOM is it's diffing mechanism (more diffing specifics here). And, the goal of this diffing mechanism as well as purpose of Virtual DOM is to minimize the likelyhood of janky experiences.

But, wouldn't handlling Virtual DOM -> DOM -> Browser UI consume more resources than DOM -> Browser UI?

Virtual DOM allows the framework (React, Vue) to optimize the re-render requests received from our declarative code. This is to minimize the likelyhood of re-renders leading to unresponsive UIs.

How can diffing & re-rendering be faster than re-rendering?

Since the Virtual DOM introduces an additional layer of object-structure to maintain, it requires additional compute & memory. All the diffing cannot be for free. As a result, rendering to the Virtual DOM has to be slower than rendering directly to the DOM. And, this has been reflected via benchmarks (see here) and opinions (see here).

But, the goal of the Virtual DOM isn't to be faster than rendering to the DOM. It is to prevent the potential re-rendering landmines that the abstraction of declarative programming brings. For all it's easier to debug & maintenance benefits, declarative prgramming can lead to re-rendering pitfalls. As a result, our declarative code has to be optimized for rendering. And Virtual DOM is one way to achieve this to minimize the rendering performance pitfalls.

The goal of the Virtual DOM isn't to be faster than rendering directly to the DOM. It is to prevent the potential re-rendering landmines that the abstraction of declarative programming brings.

Conclusion

Frequent re-rendering on the UI can lead to unresponsive pages & janky experiences. More so with modern web-apps that have many interactive elements on the page. And, even more so with the abstraction of declarative programming (that React offers).

Achieving reliable rendering performance for such apps requires a mechanism that can optimize the render requests fired by declarative UI code before being passed to the browser DOM. Virtual DOM, with it's diffing mechanism, achieves this for frameworks like React & Vue.

All in all, Virtual DOM allows us to have all the declarative programming goodness while it keeps a check over the potential rendering performance pitfalls.


Punit Sethi

About the Author

Punit Sethi has been working with large e-Commerce & B2C websites on improving their site speed, scalability and frontend architecture. He tweets on these topics here.