Virtual DOM: The Real Reason React is Fast
Starting from the Question: Why is DOM Manipulation Slow?
When I first learned frontend development, a senior developer told me: "DOM manipulation is expensive, so minimize it." I didn't understand what that meant. Writing document.getElementById().innerHTML = "new text" in JavaScript seemed simple. What's expensive about changing a string? But as I used React and heard people say "Virtual DOM improves performance," I wanted to truly understand what this meant.
It came down to this: the browser's process of redrawing the screen is expensive. When you manipulate the DOM, the browser doesn't just change a value in memory—it reruns the entire rendering pipeline. Once I understood this rendering pipeline, the need for Virtual DOM clicked.
Browser Rendering Pipeline: The Core of Why It's Slow
When a browser receives HTML, it goes through these steps:
- Build DOM Tree: Parse HTML into a DOM tree.
- Build CSSOM Tree: Parse CSS into a CSSOM tree.
- Create Render Tree: Combine DOM and CSSOM to get elements that will actually be visible.
- Layout (Reflow): Calculate exact positions and sizes. "This div should be 100px from left, 200px from top, with 300px width."
- Paint: Fill pixels with colors—text color, background, borders.
- Composite: Merge multiple layers into the final screen.
Layout (Reflow) is the most expensive step. When one element's size or position changes, surrounding elements need recalculation. For example, if you add an item to the top of a list, all items below get pushed down. The browser must recalculate all their Y coordinates.
I understood this as "subway seat shifting". When someone squeezes in the middle, everyone behind shifts one seat over. Similarly, when one DOM element changes, surrounding elements are affected in a chain reaction.
Accepting the Concept of Virtual DOM
Initially, "creating a fake DOM in memory" seemed odd. "Why not just modify the real DOM directly?" But observing how React works, I understood: Virtual DOM acts as a "buffer that batches changes for single processing".
Apartment Remodeling Analogy: How I Understood It
I accepted it this way:
- Real DOM: The actual apartment I live in. Changing wallpaper requires calling a professional, moving furniture—it's chaotic and costly every time.
- Virtual DOM: A 3D blueprint file of the apartment. In a computer program (like SketchUp), I can move walls, change wallpaper colors, rearrange furniture—all without spending money. It's just mouse clicks.
What React does:
- Simulate multiple changes on the blueprint (Virtual DOM).
- Compare the old blueprint with the new one (Diffing).
- "Ah, what actually changed is the living room wallpaper color and bedroom furniture position."
- Apply only those changes to the real apartment (Real DOM).
It came down to this: minimizing unnecessary construction is the core. If the state changes 10 times but the final result matches the initial state, the Real DOM isn't touched at all. Everything happens in simulation on the Virtual DOM.
React's Reconciliation Process: Diving Deep
Reading React's official documentation, I properly understood Reconciliation. It's not just "comparing"—the efficient comparison algorithm is key.
1. Render Phase: Creating New Virtual DOM
When state or props change, React re-executes the component. The JSX in the return statement converts to a Virtual DOM object.
// Example: Counter component
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
// Virtual DOM when count is 0 (simplified)
{
type: 'div',
props: {},
children: [
{ type: 'h1', props: {}, children: ['Count: 0'] },
{ type: 'button', props: { onClick: [Function] }, children: ['Increase'] }
]
}
// When count becomes 1, new Virtual DOM
{
type: 'div',
props: {},
children: [
{ type: 'h1', props: {}, children: ['Count: 1'] }, // Only this changed!
{ type: 'button', props: { onClick: [Function] }, children: ['Increase'] }
]
}
This process is pure JavaScript object manipulation, so it's extremely fast. No browser involvement needed.
2. Diffing: Spot-the-Difference Algorithm
React compares the previous Virtual DOM with the new one. Perfectly comparing tree structures requires O(n³) time complexity (extremely slow). So React made two assumptions.
Assumption 1: Different element types produce completely different trees.
// Previous
<div><span>Hello</span></div>
// New
<div><p>Hello</p></div>
// span changed to p, so remove span and create new p
Assumption 2: List items can be identified by key prop.
// Without keys
<ul>
<li>A</li>
<li>B</li>
</ul>
// Adding item C at the beginning
<ul>
<li>C</li> // React: "First changed from A to C? Remove A, create C."
<li>A</li> // "Second changed from B to A?"
<li>B</li> // "Third is new B."
</ul>
// Result: Inefficiently recreates everything
// With keys
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// Adding C
<ul>
<li key="c">C</li> // React: "key='c' is new. Just add this."
<li key="a">A</li> // "key='a' unchanged. Leave it."
<li key="b">B</li> // "key='b' unchanged too."
</ul>
I understood this as "finding students by student ID". Names (content) can change, but student IDs (keys) are unique, so you can determine "this student is the same, that student is new" by key.
3. Commit Phase: Applying to Real DOM
Only changes found by Diffing get applied to Real DOM. In the example above, only the <h1> tag's text changes from "Count: 0" to "Count: 1". The <div> and <button> remain untouched.
In this process, React uses Fiber Architecture (since React 16). Fiber breaks work into small units, allowing the browser to handle urgent tasks (like user input) first, then resume rendering. I thought I should study this separately later.
Optimization Techniques: Reducing Unnecessary Renders
React optimizes automatically, but developers can give hints. I accepted these three approaches.
React.memo: Component Memoization
If props haven't changed, don't re-render the component.
const ExpensiveComponent = React.memo(({ data }) => {
console.log("Rendered!");
return <div>{data}</div>;
});
function Parent() {
const [count, setCount] = React.useState(0);
const data = "Fixed data";
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveComponent data={data} />
{/* Even if parent re-renders, ExpensiveComponent won't if data didn't change */}
</div>
);
}
useMemo: Caching Computation Results
Don't recompute expensive calculations every time—cache the result.
function FilteredList({ items, filterText }) {
const filteredItems = React.useMemo(() => {
console.log("Filtering...");
return items.filter(item => item.includes(filterText));
}, [items, filterText]); // Recalculate only when items or filterText changes
return <ul>{filteredItems.map(item => <li key={item}>{item}</li>)}</ul>;
}
useCallback: Preventing Function Regeneration
When passing functions as props, don't create new functions every time.
function Parent() {
const [count, setCount] = React.useState(0);
// Without useCallback, new function created each time, causing Child to re-render
const handleClick = React.useCallback(() => {
console.log("Clicked!");
}, []); // Empty dependency array means function never changes
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</div>
);
}
const Child = React.memo(({ onClick }) => {
console.log("Child rendering");
return <button onClick={onClick}>Click me</button>;
});
Other Frameworks' Approaches: Virtual DOM Isn't the Only Answer
While studying React, I became curious how other frameworks handle this.
Vue: Uses Virtual DOM But Smarter
Vue also uses Virtual DOM, but unlike React, it has a Reactivity System. Vue tracks which data affects which components. When data A changes, it knows precisely "only component X needs re-rendering." React, on the other hand, re-renders all child components when state changes, then filters through Diffing, so Vue can theoretically be more efficient.
Angular (Ivy): Incremental DOM
Angular's Ivy renderer uses Incremental DOM. Unlike Virtual DOM, it doesn't keep two trees (old and new) in memory. Instead, it directly traverses the Real DOM and updates only necessary parts. This uses less memory. I understood this as "on-site construction"—instead of drawing two blueprints, you go on-site and directly say "change this, change that."
Svelte: Compile-Time Optimization
Svelte doesn't use Virtual DOM at all. During build time, it generates code like "when variable A changes, update DOM node B." No runtime overhead, making it extremely fast. I accepted this as "pre-assembled at factory and shipped". Instead of assembling (Diffing) on-site (browser), you create the finished product at the factory (build time) and send it.
Virtual DOM's Limitations: When Does Overhead Become a Problem?
Virtual DOM isn't omnipotent. The case that resonated with me is very large lists.
For example, if a table has 10,000 rows updating on scroll, Virtual DOM Diffing itself becomes burdensome. In such cases, use virtualization. Only render the 100 visible rows in the DOM, swapping top and bottom as you scroll (libraries like react-window, react-virtualized).
Another case is animations. Processing 60fps animations through Virtual DOM can be costly due to Diffing overhead. In these cases, using CSS animations or the Web Animations API directly is better.
React 18's Concurrent Features: Evolution of Virtual DOM
Recently, React 18 introduced Concurrent Features. While not directly related to Virtual DOM, I understood it as an extension of rendering optimization.
- useTransition: Defer non-urgent updates. Prioritize urgent tasks like user input first.
- useDeferredValue: Delay value updates to maintain smooth UI.
- Suspense for Data Fetching: Show fallback UI during data loading, render smoothly when ready.
This is possible because Fiber Architecture can break work into chunks and prioritize them. It came down to this: Virtual DOM exists not just "to be fast," but "to improve user experience" and continues evolving.
Summary: Virtual DOM is a Product of Trade-offs
The core I accepted from studying Virtual DOM:
- Why DOM manipulation is expensive: Browser rendering pipeline (Layout, Paint, Composite).
- Essence of Virtual DOM: A buffer that simulates changes in memory first, applying only final changes to Real DOM.
- Core of Reconciliation: Diffing algorithm finds changes in O(n) time. Why key prop matters.
- Performance optimization: Reduce unnecessary renders with React.memo, useMemo, useCallback.
- Alternative approaches: Vue's reactivity system, Angular's Incremental DOM, Svelte's compile optimization.
- Limitations and solutions: Virtual scrolling for large lists, CSS for animations.
- Future direction: React 18's concurrent features for better UX.
Ultimately, Virtual DOM isn't "absolutely fastest" but "a trade-off for maintaining code maintainability while achieving sufficiently fast performance". I understood it this way, and now when writing React code, it's clear "why I should use keys" and "why I shouldn't overuse useMemo."