So you've been using React for a while, calling setState, watching your UI update magically, and somewhere in the back of your head you've been wondering... how does this actually work? Like, what is React actually doing between the moment you change some state and the moment your screen updates?
That's exactly what we're going to break down today. No fluff, no theory marathons. Just a clear, step-by-step walkthrough of what's really happening under the hood.
First, Let's Talk About the Real Problem
Before Virtual DOM even makes sense, you need to understand what problem it's solving.
The browser has something called the Real DOM (Document Object Model). Think of it as a giant tree of objects that represents every element on your page. Every <div>, every <button>, every <p> tag lives in this tree. When you update something, the browser has to repaint, reflow, and recalculate layouts. And doing that frequently? It's expensive. To understand how a browser works, check out this blog.
Here's the thing about direct DOM manipulation: it's not the reading that's slow, it's the writing. Every time you touch the DOM, the browser kicks off a cascade of work. If you're updating ten different elements separately, you're triggering that cascade ten times. In older jQuery-style apps, developers would do things like this:
document.getElementById('user-name').innerText = 'John';
document.getElementById('user-age').innerText = '25';
document.getElementById('user-avatar').src = 'john.jpg';
// ...and so on for every little thing that changed
Each one of those is a direct punch to the Real DOM. And as your app grows, those punches add up fast. The UI stutters, animations drop frames, and users start noticing.
React's answer to this was: what if we stop going directly to the DOM every single time something changes?
Enter the Virtual DOM
The Virtual DOM is essentially a lightweight JavaScript object that mirrors the structure of the Real DOM. It's not a browser thing, it's just plain JavaScript living in memory.
When React renders your components, it doesn't immediately write to the Real DOM. Instead, it first builds this Virtual DOM tree, a nested JavaScript object that describes what the UI should look like. Something like this internally:
{
type: 'div',
props: { className: 'container' },
children: [
{
type: 'h1',
props: {},
children: ['Hello, John']
},
{
type: 'p',
props: { className: 'bio' },
children: ['Frontend Developer']
}
]
}
This is basically what your JSX compiles down to. That <div className="container"> you write? React transforms it into something like the object above using React.createElement() calls behind the scenes.
Creating and comparing JavaScript objects is super cheap compared to touching the browser's DOM. That's the whole game here.
The Initial Render: How It All Starts
When your React app loads for the first time, here's exactly what happens:
Step 1: React takes your root component (usually <App />) and starts calling your component functions one by one, top to bottom.
Step 2: Each component returns JSX, which gets compiled into React.createElement() calls, building up that Virtual DOM tree we talked about.
Step 3: Once the full Virtual DOM tree is ready, React takes it and does a single, efficient pass to build the Real DOM and inject it into your <div id="root">.
This first render is actually doing real DOM work, React has no choice here because the page is blank. But it does it in one shot, building everything in memory first and then committing it to the browser in one go. This is already smarter than doing a bunch of individual DOM manipulations.
Now Something Changes: State or Props Update
This is where the magic really happens. Let's say a user clicks a button and you call setState. Here's the sequence of events React kicks off:
Step 1: React marks the component as "needs re-render."
Step 2: React calls your component function again and produces a brand new Virtual DOM tree. This is a fresh snapshot of what the UI should look like after the change.
Step 3: React now has two Virtual DOM trees sitting in memory: the old tree (what the UI currently looks like) and the new tree (what it should look like after the update).
Step 4: React runs a process called diffing, comparing the old tree to the new tree to figure out exactly what changed.
Step 5: React collects all the differences into a minimal set of changes, called a patch, and applies only those changes to the Real DOM.
That last part is the key insight. Instead of re-rendering everything, React surgically updates only what actually changed.
Diffing: How React Compares Two Trees
Diffing is the process of comparing the old Virtual DOM tree with the new one. The technical term React uses is reconciliation. Let's make this concrete.
Imagine your old tree looked like this:
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
</ul>
And after a state change, the new tree looks like this:
<ul>
<li>Apple</li>
<li>Mango</li> <-- changed
<li>Cherry</li>
</ul>
React walks through both trees simultaneously, node by node. When it hits <li>Banana</li> in the old tree and <li>Mango</li> in the new tree, it flags that as a change. Everything else matches, so React only updates that one text node in the Real DOM. One update. Not three.
React's diffing uses two important heuristics to stay fast:
1. Elements of different types produce completely different trees. If you switch a <div> to a <span>, React doesn't try to figure out what to reuse. It just tears down the old subtree and builds a new one from scratch.
2. The key prop helps React track list items. This is why React warns you when you render a list without keys. Without keys, if you insert an item at the top of a list, React thinks every single item changed because positions shifted. With keys, React can figure out "oh, this item just moved, it didn't change" and skip unnecessary updates.
// Without keys: React gets confused on reorder/insert
{items.map(item => <li>{item.name}</li>)}
// With keys: React tracks each item correctly
{items.map(item => <li key={item.id}>{item.name}</li>)}
Minimal Updates: Updating Only What Changed
So React has figured out the diff. Now what?
React batches all those changes into what's called a commit phase. Instead of applying each change one at a time (which would trigger multiple browser repaints), React collects all necessary DOM operations and applies them together in a single pass.
This is huge for performance. The browser gets to do its expensive reflow/repaint work exactly once, no matter how many things changed in your component tree.
Let's think about this with an example. Say you have a dashboard with 50 components. The user updates their profile name. Without Virtual DOM, you might naively re-render every component touching the DOM 50 times. With React's approach, React re-renders the components in memory (cheap, it's just JavaScript), figures out only the name text node changed, and makes exactly one targeted update to the Real DOM.
Why This Actually Improves Performance
Let's put it all together with a clear picture:
Direct DOM manipulation forces you to write individual DOM operations manually. If you're not careful, you'll trigger multiple reflows and repaints. At scale, this becomes a performance nightmare.
React's Virtual DOM approach gives you a much smarter pipeline. You describe what your UI should look like, React figures out how to get there with the fewest possible DOM operations, and the browser has to do the least amount of expensive work.
It's like the difference between giving someone directions one turn at a time ("turn left, now turn right, now go straight...") versus handing them a full route upfront so they can optimize the path themselves.
There's also a developer experience benefit here. You write declarative code ("here's what the UI should look like") instead of imperative code ("do this, then that, then this"). React handles the imperative DOM manipulation internally so you don't have to think about it.
The Full React Update Lifecycle
Let's put the entire thing together in one clean mental model. Every update in React goes through three phases:
Render Phase - React calls your component functions and builds the new Virtual DOM tree. This is pure JavaScript computation, nothing touches the browser yet. React can pause, abort, or restart this phase if needed.
Diff Phase - React compares the new Virtual DOM tree against the previous one (reconciliation). It walks the two trees and builds a list of changes. Still no Real DOM involvement.
Commit Phase - React takes the list of changes and applies them to the Real DOM all at once. This is the only point where the browser gets involved. After this, effects like useEffect run.
Putting It All Together
Here's the simplest way to think about it. The Real DOM is like a physical whiteboard in an office. Erasing and redrawing stuff on it takes time and effort. The Virtual DOM is like a personal notebook. Writing in your notebook is fast, comparing two pages in your notebook is fast. Only once you know exactly what needs to change do you walk up to the whiteboard and make the minimum number of edits.
React is basically saying: let me do all the thinking in my notebook first, and I'll only bother the whiteboard when I know exactly what to write.
That's the entire mental model. Your components describe what the UI should look like. React keeps a copy of that description in memory as the Virtual DOM. When something changes, React builds a new description, compares it to the old one, finds the delta, and applies only that delta to the Real DOM. Fast, efficient, and you never have to think about DOM manipulation manually.
Once you internalize this flow, a lot of React's other behaviors start making more sense too. Why batching state updates matters. Why keys in lists are important. Why useMemo and React.memo exist. They're all tools that work with or optimize different parts of this same pipeline.
Hope you found this helpful! If you spot any mistakes or have suggestions, let me know. You can find me on LinkedIn and X, where I post more about web development.
















