Misconceptions about Virtual DOM

Alex Yakunin
ITNEXT
Published in
4 min readMay 14, 2021

--

If you believe Virtual DOM is a “lightweight copy” of a real DOM helping to speed up the updates by batching them together— continue reading. Albeit being correct, it’s a highly misleading answer. Surprisingly, it’s also the most popular one: googling for “react virtual DOM” leads you to very similar descriptions: 1, 2, and you get ~ the same results even if you stick to the top answers on StackOverflow: 1, 2, 3;

The content below equally applicable to React and Blazor.

Why it’s a misleading answer?

#1. It doesn’t highlight the key problem Virtual DOM solves — namely, the elimination of unnecessary re-renders. And when I say “re-renders”, it’s not about re-rendering of certain DOM elements on browser UI update cycles. It’s about not calling render() methods of React components which are known to produce the same output, which effectively eliminates re-rendering of the whole subtrees of components.

If you know JS and React, check out this example. It logs
"TimeString.render(): ..." message every second, even though Clock component re-renders itself 10 times per second (see setInterval call on line 16) and produces an output with TimeString component on every render. So why TimeString logs its render just once per second rather than 10? That’s because TimeString extends PureComponent, which overrides shouldComponentUpdate to ensure re-rendering happens only once props or state changes, and time property is a string that changes just once per second. If you change TimeString’s base type to Component, it will start reporting 10 renders per second.

Now, notice that to make it work, something should call shouldComponentUpdateon existing component. In other words, Virtual DOM diffing algorithm has to conclude that TimeString component used in prev. render is actually the same component as we’ve just rendered. And this is exactly what happens during the reconciliation:

  • Changes made to DOM elements are applied to the real DOM — almost every description of Virtual DOM highlights just this part of the equation.
  • All mounted React components decide whether they have to re-render due to possible changes in props — and this crucial part is somehow frequently ignored, even though it’s the one responsible for the biggest savings.

Below is an illustration of how diffing works in Blazor (which copies React component model almost exactly):

  • <div class="main"> — a node describing DOM element — doesn’t require any action
  • <Welcome> component got property change, so it will be re-rendered
  • <Clock> component didn’t exist earlier, so it will be rendered and mounted
  • <div> element doesn’t have a match now, so it will be removed from DOM.

#2. How many instances of Virtual DOM exist in your app? Is it a single component tree per app, or maybe one Virtual DOM per every root component? AFAIK most of developers stick to the later point.

Interestingly, Virtual DOM is simply the output of component’s render()method. Look at the code of App class in above example:

class App extends React.Component {
render = () =>
<div class="main">
<Welcome name="World" />
<Clock />
</div>
}

JSX transpiler converts it to the following code:

class App extends React.Component {
constructor(...args) {
super(...args);
_defineProperty(this, "render",
() => /*#__PURE__*/
React.createElement("div", {
class: "main"
}, /*#__PURE__*/
React.createElement(Welcome, {
name: "World"
}), /*#__PURE__*/
React.createElement(Clock, null)));
}
}

See react.createElement(...) calls there? They literally define the nodes of a tree. This tree is compared with the previous one (for the same component) during the reconciliation, where the “previous one” is the same tree, but produced by the previous call to render()method and referencing actual (mounted) React components.

And as you might guess, this “local Virtual DOM” is not “embedded” into a “big Virtual DOM” explicitly, i.e. Virtual DOM is local to each component. Though technically such trees still form a single “big Virtual DOM” tree for any root React component, because:

  • Each React component remembers its last render() output, which is its local Virtual DOM
  • Each Virtual DOM node describing a React component references actual (mounted) component — which in turn keeps track of its own local Virtual DOM, and so on.

That’s it :)

P.S. I wrote this post as a prequel for a few more posts explaining why immutable models are such a good fit for React and Blazor, why all popular UI architectures (MVC, MVVM / MobX, Recoil, Flux/Redux, etc.) are actually quite similar, and finally, why you don’t need any of them if you use Fusion. Stay tuned!

--

--