Skip to main content

Command Palette

Search for a command to run...

How React Virtual DOM works under the hood

Updated
10 min read
How React Virtual DOM works under the hood

If you have been creating web application through DOM manipulation before REACT, then this topic would feel quite natural to you, but in case, you don't have much experience creating things by DOM manipulation, just have a quick go in this article.


What is the problem with DOM

As evident, earlier devs manipulated the DOM directly using JS. This sounds simple but we need to realise that browser's DOM isn't lightweight data structure, it's complex, living tree of nodes, each node carries a heavy payload, styles, event listeners, layout information, metadata and more.

Every time we make a change in DOM, the browser does two important things:

  • Reflow: recalculate the position and size of elements on the page

  • Repaint: redraw the pixels on screen.

Now these two process take time and imagine having a 1000 nodes, the DOM updates each node one by one in a loop, then this is slow, and visually painful. Identifying this issue is the core idea of React, it understands that the problem isn't in JS, because JS itself is fast, the bottleneck is touching the DOM too often and too carelessly.

Real DOM vs Virtual DOM

DOM stands for Document Object Model. It is the internal representation of your HTML in the form of a tree where each HTML element is a node and each node can have further nodes as children. A single DOM node exposes a lot of properties so creating even one is not cheap.

To solve the problem of making changes directly in DOM, we have a concept of creating a copy of it as a plain JS object that describes what the UI should look like. You can say we maintain a light weight copy of the real DOM tree. This is what is popularly known as Virtual DOM (VDOM).

To put it in simple terms, VDOM is aa blueprint of the real DOM, cheap to create, modify and compare

{
  type: 'div',
  props: {
    className: 'card',
    children: [
      {
        type: 'h1',
        props: { children: 'Hello World' }
      }
    ]
  }
}

Below is a simplistic way of representing a VDOM in terms of plain JS object. It has no browser APIs, no layout calculation, no repaints, just data.

Getting Virtual DOM to work

Now that we have understood, what VDOM is, let us understand, how it is created and the way it works:

1. Initial Render

The very first time when you application loads, you don't have the VDOM created before hand, you start by creating the actual DOM itself but what is the order of creation and how it's done is stated below:

In an actual React application, inside the main.jsx file, you must have seen something like:

createRoot(document.getElementById("root")!).render(<App />);

This is React calling your components starting from and working down through all the child components.

Your react child components, might look something like:

const ChildComp= ()=>{
  return <div> child component here </div>
}

Babel compiles each component return JSX into React.createElement(). Those calls produce a tree of plain JS objects. These calls are responsible for creating a tree of plain JS objects which is the initial virtual DOM tree.

React then takes that Virtual DOM tree and uses it to build the actual Real DOM nodes and insert them into the browser.

Now the virtual DOM is stored in the memory and React holds onto it. On initial render, there's nothing to diff against, so the real DOM is directly built from the virtual DOM.

2. Trigger Re-render

A re render is triggered when react notices that there's some change in your app. it could be a user clicking a button or an API respond or a timer starting, these actions cause state or props update.

Now React does not immediately touch the real DOM, instead it schedules a re-render of the component(s) whose state or props changed, plus all their children (unless your use React.memo)

Re-rendering children is a smart move because by default if a parent's output has changed then the children might have new props as well. React plays safe over here.

3. New Virtual DOM tree

React re-runs the affected components. It calls their render methods (or function bodies) again and collects the new JSX output.

This produces a new Virtual DOM tree , a fresh JS object tree describing what the UI should look like after the state change.

Now React has two things in the memory, the initial DOM tree, the one you had saved in the first render and the new virtual DOM tree which is created out of the updated UI looks.

Both are just JavaScript objects. Comparing them is fast. This is where diffing begins.

4. Diffing

Now you have an actual DOM, and two VDOM, you need to make comparisons in the two virtual DOMs to find what actually changed.

The naive approach is comparing the two trees which is an O(n³) algorithm. This is unusable, so React uses a set of smart heuristics to bring this down to O(n) i.e. linear time. Here's how:

  1. Elements of Different Types Are Replaced Entirely
    If the old tree had a <div> and the new tree has a <section> in the same position, React doesn't try to patch it. It tears down the old subtree and builds a fresh one.

    Old: <div>...</div>
    New: <section>...</section>
    → Destroy div + all children, build section from scratch
    
  2. Same Type, Same Position → Update Props
    If the element type is the same, React keeps the underlying DOM node and only updates the attributes/props that changed.

    Old: <button className="blue" onClick={handleA}>Click</button>
    New: <button className="red" onClick={handleB}>Click</button>
    → Only className and onClick are patched on the existing DOM node
    

    This is the key performance win. The DOM node is reused. No tear-down, no recreation.

  3. Using keys for Lists
    Remember, how React throws a warning in the console when you loop a list to create a JSX without providing the key? This is where things actually matter.

    Without keys, if you reorder or insert into a list, React compares items by position. Insert an item at the top, and React thinks every single item changed, it re-renders all of them.

    With key props, React tracks identity across renders. If key="user-42" moves from position 3 to position 1, React knows it's the same node and just moves it.

    // Bad — React diffs by index position
    {items.map(item => <li>{item.name}</li>)}
    
    // Good — React diffs by stable identity
    {items.map(item => <li key={item.id}>{item.name}</li>)}
    

    It's about giving React a stable identity to track across renders so it doesn't wastefully destroy and recreate DOM nodes.

5. Finding the Minimal Set of Changes

React has completed the diffing between the two trees, now it produces a change list, which is sometimes called a patch or effect list. This is the minimal set of Real DOM operations needed to bring the Real DOM in sync with the new Virtual DOM.

Examples of what might be in this list:

  • Update the className attribute on node X

  • Change the textContent of node Y

  • Insert a new <li> at position 3 in node Z

  • Remove node W entirely

The entire point of all the diffing work is to make this list as short as possible.

6. Committing to the Real DOM

Now that the diffing is complete and React has the change list with it, now it has entered the commit phase. In this phase, React will actually touch the real DOM.

React applies every change in the list to the Real DOM in one go — as efficiently as possible. Because the changes are minimal and batched, the browser's reflow and repaint cost is minimized.

After commit, the new Virtual DOM tree becomes the "old" tree. It's stored in memory for the next render cycle, when the whole process repeats.

What are the benefits ?

In earlier section, we discussed how heavy updating the DOM is, so we used JS objects for comparison and avoided unnecessary DOM operations making things lightweight.

Another issue which we resolved was continuous disturbing of the DOM, React batches multiple state updated and do a single render + commit cycle, this means that we have one DOM update instead of ten.

If there are minimal changes like changing the className then we no longer change then entire subtree for it, this is possible because of React's Diffing algo.

Diffing vs Reconcilliation

Reconciliation is React's entire system for keeping the Real DOM in sync with your component state. It answers the question: "Given that something changed, what should I do about it?"

Diffing is specifically the tree comparison algorithm React uses during reconciliation. It answers: "What is actually different between the old and new Virtual DOM?"

So when someone says "reconciliation happens," they mean the full pipeline. When they say "diffing happens," they mean specifically the comparison step.

These two terms are often loosely and interchangeably used and honestly it does not make you seem incorrect.

React Fibre

React Fiber is the complete rewrite of React's core reconciliation engine, shipped in React 16 (2017). It doesn't change what React does conceptually, the Virtual DOM, diffing, and committing still happen. It changes how React does it internally, and crucially, when.

Problem Solved by React Fibre

Before React Fibre, we had Stack Reconciler, because it used JS call stack, it had one critical flaw:

Once it started reconciling, it couldn't stop.

If you had a large component tree and state changed, React would walk the entire tree, diff everything, and commit, all in one synchronous, uninterruptible chunk of work. If that took 200ms, the browser was completely blocked for 200ms. No user input, no animations, nothing. The UI froze.

This is called blocking rendering, and it's the core problem Fiber was designed to eliminate.

You saw the 5 steps earlier:

  1. Initial Render

  2. Trigger Re-render

  3. New Virtual DOM tree

  4. Diffing

  5. Finding minimal set of changes

  6. Committing to real DOM

Now all these steps are the same before and after React fibre, the major change that React fibre has done is that step 1-5 are now interruptible, i.e. React can pause them, handle urgent work like user input and then decide if it wants to resume or restart again.

Stack Reconciler Flow:

setState triggered
    ↓
Step 1: Initial render logic starts
    ↓
Step 2: Trigger re-render
    ↓
Step 3: New Virtual DOM tree created
    ↓
Step 4: Diffing happens
    ↓
Step 5: Minimal changes found
    ↓
[User clicks a button... but too bad, you have to wait]
    ↓
Step 6: Commit to Real DOM
    ↓
Done. NOW the user's click gets processed.

If steps 1–5 took 200ms, the user waited 200ms to see their click happen. The browser was completely blocked.

React Fibre Flow:

setState triggered
    ↓
Step 1: Initial render logic starts
    ↓
Step 2: Trigger re-render
    ↓
Step 3: New Virtual DOM tree created
    ↓
[User clicks a button... React detects it!]
    ↓
⏸️ PAUSE steps 1–5 (or discard and restart)
    ↓
Handle user's click (high priority)
    ↓
▶️ Resume or restart steps 1–5 with new information
    ↓
Step 6: Commit to Real DOM
    ↓
Done. User saw instant feedback.

Now the user's click is handled immediately, not blocked by render work.

Conclusion

Knowing React internals is a great thing, you can actually visualise how things, are working, It makes you understand things and the reason behind them, like up until now, every time you saw a warning for using keys in a list, or maybe used keys without realising how it affects. But now you know why.

Happy Coding !

S

The explanation of the O(n³) to O(n) heuristic is spot on. It's crazy to think about how much performance we'd lose without those specific rules for diffing element types and keys. I'll definitely be more mindful of those console warnings about key props from now on. Happy coding!

S

Niceeee