Difficulty: Intermediate
What is the Virtual DOM? How does React's reconciliation algorithm work?
Alright, let's break down the Virtual DOM and reconciliation, because this is one of those topics that sounds intimidating but is actually pretty intuitive once you see the big picture.
So here's the problem React is solving. The real DOM - the actual tree of HTML elements the browser renders on screen - is slow to manipulate directly. Not because any single change is catastrophically expensive, but because when you start making lots of changes (which modern UIs absolutely do), things add up fast. Every time you touch the DOM, the browser might have to recalculate layouts, repaint pixels, reflow elements. It's like renovating a house by tearing down walls one at a time instead of drawing up a blueprint first and doing it all in one pass.
That's exactly what the Virtual DOM is: React's blueprint. It's a lightweight JavaScript object that mirrors the structure of the real DOM. When you write JSX like <div className="card"><h1>Hello</h1></div>, React doesn't immediately go create those DOM nodes. Instead, it builds a plain JavaScript object - a virtual representation - that describes what the UI should look like.
Now here's where it gets clever. When something changes - say a user clicks a button and some state updates - React doesn't just blow away the entire DOM and rebuild it. Instead, it creates a brand new virtual DOM tree that reflects the updated state. Then it takes this new tree and compares it against the previous virtual tree. This comparison process is called reconciliation, and the algorithm that does the comparison is called the diffing algorithm.
The diffing algorithm is actually quite smart about being efficient. A naive tree-comparison algorithm would be O(n cubed), which is completely unusable for any real application. React makes two key assumptions that bring this down to O(n) - linear time. First, if two elements have different types (say a <div> changed to a <span>), React doesn't even bother comparing their children. It tears down the entire old subtree and builds a fresh one. Second, developers can provide keys on lists of elements to hint at which items are which across renders. These two heuristics work remarkably well in practice.
So the diffing process works roughly like this. React walks through both trees simultaneously, comparing nodes. Same type? Great, keep the DOM node, just update any changed attributes. Different type? Destroy the old node and all its children, mount a completely new subtree. For lists of children, React uses keys to figure out which items moved, which were added, and which were removed, instead of naively re-rendering everything.
Once React figures out the minimal set of changes needed, it enters the commit phase, where it actually touches the real DOM. This is the only part where the browser does real work - and because React has already figured out exactly what changed, it's doing the absolute minimum necessary.
Now, there's an important nuance here that trips people up in interviews. The Virtual DOM does NOT make React faster than hand-optimized vanilla JavaScript. If you were a superhuman developer who knew exactly which DOM nodes to touch and when, you could absolutely beat React's performance. What the Virtual DOM gives you is a developer experience that's fast enough while being dramatically easier to reason about. You just declare what your UI should look like given the current state, and React figures out the most efficient way to make it happen. That's the real win.
React Fiber, introduced in React 16, is the engine that makes all of this work under the hood. Before Fiber, React's reconciliation was synchronous - once it started diffing, it had to finish the entire tree before giving control back to the browser. This meant large updates could freeze the UI. Fiber reimplemented the reconciliation engine to be incremental. It breaks the work into small units called fibers, and it can pause, resume, or even abort work based on priority. A user typing into an input is higher priority than updating a background chart, and Fiber lets React handle that intelligently.
With React 18 and concurrent features, Fiber enables things like startTransition (marking some updates as lower priority), Suspense for data fetching, and automatic batching of state updates. These are all built on top of Fiber's ability to split rendering work into interruptible chunks.
One practical thing to keep in mind: the quality of your keys in lists directly impacts reconciliation performance. Using array indices as keys is a classic mistake. If you insert an item at the beginning of a list with index keys, React thinks every single item changed because all the indices shifted. With stable unique IDs as keys, React correctly identifies that items just moved positions and can reuse existing DOM nodes. This matters a lot in production when you have long scrollable lists.
// BAD - using index as key
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
// GOOD - using unique id
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
// WHY? When items are reordered, index keys
// cause React to update all items.
// Unique keys let React identify moved items
// and reuse existing DOM nodes.
Stable, unique keys help React's diffing algorithm match elements correctly across re-renders. Index keys break when items are reordered, inserted, or deleted.
import { memo, useState } from 'react';
// This component only re-renders if props change
const ExpensiveList = memo(function ExpensiveList({ items }) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
});
function App() {
const [count, setCount] = useState(0);
const [items] = useState([{ id: 1, name: 'Item 1' }]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{/* Does NOT re-render when count changes */}
<ExpensiveList items={items} />
</div>
);
}
React.memo performs a shallow comparison of props. Since items reference doesn't change, ExpensiveList skips re-rendering when count updates.
// Rule 1: Different element types -> destroy old, create new
// <div><Counter /></div> → <span><Counter /></span>
// Counter component is unmounted and remounted
// Rule 2: Same element type -> update attributes
// <div className="old" /> → <div className="new" />
// Only className is updated, DOM node is reused
// Rule 3: Same component type -> update props
// <Counter count={1} /> → <Counter count={2} />
// Same instance, componentDidUpdate / useEffect runs
// Rule 4: Keys identify children across renders
// <li key="a">A</li> → <li key="b">B</li>
// <li key="b">B</li> <li key="a">A</li>
// React knows A and B just swapped positions
These are the core heuristics React uses to achieve O(n) diffing instead of O(n³) for tree comparison.
Virtual DOM, Reconciliation, Diffing Algorithm, Fiber