If you are working with react or planning to become a react developer, you should know that the react virtual DOM will be an inescapable question in a react developer interview. You know, preparing for an interview can be frustrating, there are so many things to study, to understand, and maybe like me, you have to re-study concepts that are supposed you already know (because you're applying for a senior position 😵💫) but to be honest I've found myself studying this concept over and over again and that just means that I really don't understand how it works! 🤦🏻♀️
So, with this post I’ll try to do my best for explaining how the virtual DOM works, maybe this can’t be useful for anyone else but for me ( hopefully, it can help someone else 🤞🏻) but I read sometimes you retain more information if you write it down, so this is my experiment to see if this really works. So, without further ado let’s start.
Before starting talking about virtual DOM, let’s do a brief resume of how the Browser DOM (Document Object Model) works.
What is the Browser DOM?
When you make a request to a server to fetch the content of a page, the server returns a file in binary stream format (basically ones and zeros) with a specific content type for example Content-Type: Text/html; charset=UTF-8
this tells to the browser that it is a HTML document (also could be an XML document) and is encoded in UTF-8. With this information, the browser can read the HTML code. Initially, for every HTML tag the browser will create a Node, for example, the tag <div>
element is created from HTMLDivElement
which inherits from Node Class. When all elements are created, the browser creates a tree-like structure with these node objects. And it will look like this:
The DOM is also an API where you can access these nodes to read and modify, and that is made through the document
object using for example document.querySelector(”p”)
(Read more here)
What is virtual DOM?
The virtual DOM (VDOM) is a programming concept where a “virtual” representation of the UI (User Interface) is kept in memory (browser memory) and synced with the “real” DOM (the browser DOM 👆🏻) and this is made by a library such as ReactDOM
. This process is called Reconciliation.
In other words, React makes a copy of the “real” DOM and compares the nodes between the virtual and the real DOM to see what nodes changed, which were added, deleted, or updated. Once the differences are identified, React just updates the nodes that differ and that is the key to their great performance.
Let’s put it all together 🧐 —When we load the first time a website, our browser creates by default a data structure in memory (aka DOM) which is a node tree, where every node represents an HTML tag along with its properties. React has to create a virtual representation of this, but it has to be in an efficient way. So, how react does do that? 🤔 well, due to the DOM could be really big and complex to manipulate, React creates a smaller copy storing only the DOM part that it really will use and this usually is the div root
.
ReactDOM.render(element, document.getElementById('root'))
In the beginning, our browser has an empty structure just with the root node <div id=”root”>
, react creates a virtual DOM with all the structure that we are adding into our principal component for example the <App/>
, and when the ReactDOM.render()
method is executed, all the nodes existing on the virtual DOM are pushed to the real DOM.
The first time ReactDOM.render()
will rend the whole application, but after this first render, react will detect the changes on the different nodes and compare the before state with the new one and apply the render just for these nodes that have changed.
Note: render
has been replaced with createRoot
in React 18 read more
What happens during the render?
Is important to understand what happens during the render. For that, we need to know how React works with native HTML tags and with the components that we have created.
So let’s review this with an example:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
console.log('___<App/>', <App/>)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
We have this pretty simple app, we are printing a console.log
with <App/>
as value and this is what it returns
As you see the type
property for <App/>
is a function, so let’s see the behavior for a native HTML tag.
console.log(<div id="test">I'm a div</div>)
Adding this console.log
of a native HTML tag we get:
here the type
is a “div”
have some props children
and id
So, why this is important? because what happens on render is that the ReactDOM library has to "transpile" all these nodes to a valid JSX code in order to be valid for the DOM. So, for components, we have a type function, and that function should be executed by reactDOM to be able to get the equivalent node valid for DOM.
console.log(App())
Adding a console.log
of the App not as a component
but as a function
, we will get:
now we have a valid type “div”
with their corresponding props
and children
, so this is a node valid to add within the DOM.
What about the Reconciliation process?
The reconciliation process is the heart ❤️ of how React really updates just the nodes that have changed, so let’s take a look at how it works.
React provides a declarative API so that you don’t have to worry about exactly what changes on every update. This makes writing applications a lot easier, but it might not be obvious how this is implemented within React.
React has to implement an algorithm in order to figure out how to efficiently update the UI to match the most recent tree. There are some generic solutions to solve this algorithmic problem of generating the minimum number of operations to transform one tree into another. However, all the generic solutions have a complexity of O(n^3) where n is the number of elements in the tree. (if you are not familiar with Big O notation I will recommend watching this video)
If we implement this on React displaying 1000 elements would require in the order of one billion comparisons. This is far too expensive. Instead, React implements a heuristic O(n) algorithm based on two assumptions:
- Two elements of different types will produce different trees.
- The developer can hint at which child elements may be stable across different renders with a
key
prop.
Elements of different types
Whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch. Going from <a>
to <img>
, or from <Article>
to <Comment>
, or from <Button>
to
<div>
<Counter/>
</div>
<span>
<Counter/>
</span>
This will destroy the old Counter
and remount a new one.
DOM Elements of the Same Type
When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes. For example:
<div className="before" title="stuff" />
<div className="after" title="stuff" />
By comparing these two elements, React knows to only modify the className
on the underlying DOM node.
Recursing on Children
By default, when recursing on the children of a DOM node, React just iterates over both lists of children at the same time and generates a mutation whenever there’s a difference.
For example, when adding an element at the end of the children, converting between these two trees works well:
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React will match the two <li>first</li>
trees, match the two <li>second</li>
trees, and then insert the <li>third</li>
tree.
If you implement it naively, inserting an element at the beginning has worse performance. For example, converting between these two trees works poorly:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React will mutate every child instead of realizing it can keep the <li>Duke</li>
and <li>Villanova</li>
subtrees intact. This inefficiency can be a problem.
Importance of Keys 😱
In order to solve this issue, React supports a key
attribute. When children have keys, React uses the key to match children in the original tree with children in the subsequent tree. For example, adding a key
to our inefficient example above can make the tree conversion efficient:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
Now React knows that the element with the key '2014'
is the new one, and the elements with the keys '2015'
and '2016'
have just moved.
You can read more about the reconciliation process here
React Fiber?
Fiber is the new reconciliation engine in React 16. Its main goal is to enable incremental rendering of the virtual DOM. This is a complicated concept, basically, this new algorithm is a reimplementation of older versions of the React reconciler, has some improvements on prioritizing the order of how things are rendered, breaks the limits of the call stack, and lets it pause or start rendering work wherever required. You can read more here and here
Ok, I think this is the end, please let me a comment if maybe I’m wrong on something or if you feel there is something that should be added, or just if this was useful for you 😊
Thank you so much for taking the time to read it!! 🙇🏻♀️
Top comments (0)