Aug 22, 2020
This post aims to offer a jargon-free answer to the following questions:
Before we talk about the Virtual DOM, let's first understand the DOM & how the browser renders DOM updates to the user's screen:
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.getElementById('content').innerText += "A lot more paragraph text"; will change the corresponding object in the above DOM tree.
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:
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.
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 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.
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.
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.
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 likelihood of us writing code that leads to frequent expensive re-renders.
Now that we understand declarative programming & how it affects rendering DOM updates, let's talk about the 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.
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 likelihood 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 likelihood of re-renders leading to unresponsive UIs.
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.
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.