Every React developer knows โReact uses a virtual DOM.โ Few can explain what that actually means. Hereโs the full picture.
The problem: DOM updates are slow
The browserโs DOM (Document Object Model) is a tree of objects representing your HTML. Updating it is expensive because every change can trigger:
- Style recalculation
- Layout (reflow)
- Paint
- Compositing
Changing one elementโs text? Fast. Changing 100 elements? The browser might reflow the entire page.
Reactโs solution: diff first, update minimally
Instead of updating the DOM directly, React:
- Keeps a lightweight copy of the DOM in memory (the virtual DOM)
- When state changes, builds a new virtual DOM tree
- Compares the new tree with the old tree (diffing)
- Calculates the minimum set of real DOM changes needed
- Applies those changes in a batch
What the virtual DOM looks like
Itโs just JavaScript objects:
// This JSX:
<div className="card">
<h1>Hello</h1>
<p>World</p>
</div>
// Becomes this virtual DOM object:
{
type: 'div',
props: { className: 'card' },
children: [
{ type: 'h1', props: {}, children: ['Hello'] },
{ type: 'p', props: {}, children: ['World'] }
]
}
Creating and comparing JavaScript objects is orders of magnitude faster than touching the real DOM.
The diffing algorithm (reconciliation)
Reactโs diff algorithm has three rules that make it O(n) instead of O(nยณ):
Rule 1: Different types = full replace
If an element changes type (e.g., <div> to <span>), React destroys the old tree and builds a new one. No attempt to reuse.
// Before:
<div><Counter /></div>
// After:
<span><Counter /></span>
// React: destroy everything inside <div>, rebuild inside <span>
// Counter's state is lost
Rule 2: Same type = update props
If the type is the same, React keeps the DOM node and only updates changed attributes.
// Before:
<div className="old" style={{color: 'red'}} />
// After:
<div className="new" style={{color: 'blue'}} />
// React: only updates className and color. One DOM operation.
Rule 3: Keys identify list items
When diffing lists, React uses key props to match old and new items:
// Before:
<li key="a">Alice</li>
<li key="b">Bob</li>
// After:
<li key="b">Bob</li>
<li key="a">Alice</li>
// With keys: React knows to reorder, not recreate
// Without keys: React updates both elements' text content
This is why missing keys cause bugs and performance issues.
The update cycle
setState() called
โ
New virtual DOM tree created
โ
Diff old tree vs new tree
โ
List of minimal changes (patches)
โ
Batch apply to real DOM
โ
Browser repaints
React batches multiple setState calls into a single update cycle. If you call setState three times in an event handler, React diffs once and applies once โ not three times.
React 18: Concurrent rendering
React 18 added the ability to pause and resume rendering. If a high-priority update (user typing) comes in while a low-priority update (loading data) is rendering, React can:
- Pause the low-priority render
- Handle the high-priority update
- Resume or restart the low-priority render
The virtual DOM makes this possible because itโs just JavaScript objects โ you can throw away a half-built tree and start over with no DOM side effects.
Is the virtual DOM actually faster?
Controversial take: the virtual DOM isnโt faster than hand-optimized DOM updates. A developer who knows exactly which element to update can do it in one line of vanilla JS.
The virtual DOM is faster than naive DOM updates. Itโs a trade-off: you write declarative code (โhereโs what the UI should look likeโ) and React figures out the minimal DOM changes. You sacrifice some raw performance for developer productivity.
Frameworks like Svelte and SolidJS skip the virtual DOM entirely and compile to direct DOM updates. They can be faster for certain workloads. But Reactโs approach is โfast enoughโ for 99% of applications.
Related resources
Related: React Interview Questions