React's "JSX → DOM auto-updates" magic = virtual DOM + reconciliation. Yet modern frameworks (Svelte / Solid) do the same magic without a VDOM. This guide covers how VDOM works, why the diff is O(n) instead of O(n³) (a heuristic), why the key prop matters, and the no-VDOM alternative path.
What VDOM Is
Core idea: don't mutate the DOM directly — describe the desired shape as a JS object.
JSX:
<div className="card">
<h1>{title}</h1>
<p>{body}</p>
</div>
→ After compilation:
React.createElement("div", {className: "card"}, [
React.createElement("h1", null, title),
React.createElement("p", null, body)
])
→ JS object at runtime (the VDOM tree):
{type: "div", props: {className: "card"}, children: [
{type: "h1", props: {}, children: [title]},
{type: "p", props: {}, children: [body]}
]}
State change → new VDOM tree (a different object from the previous one)
→ React diffs new vs old (reconciliation) → only the actual DOM
diffs are applied.Why VDOM — Compared to Direct DOM
Direct DOM (jQuery era):
$("#list").empty();
for (let item of items) {
$("#list").append("<li>" + item.name + "</li>");
}
Problems:
- 100-item list → 100 DOM mutations
- One-item change still re-creates all (or you write a manual diff)
- jQuery is lightweight but app complexity ↑
VDOM's answer:
function List({items}) {
return <ul>{items.map(i => <li>{i.name}</li>)}</ul>;
}
- React diffs automatically
- 1 changed item among 100 → 1 DOM mutation
- App code is "re-render everything" — declarativeReconciliation Diff — O(n) Heuristic
The general tree-diff algorithm is O(n³) — exact minimum edit
distance between two large trees. 1000-node tree = 10^9 ops → too slow.
React's trick — two heuristics to reduce to O(n):
1. Different element types → different subtrees
<div>...</div> → <span>...</span>
→ remove the entire div + build the span fresh (no internal diff)
2. The key prop signals list reorder
Without key: <li>A</li><li>B</li><li>C</li> → <li>B</li><li>C</li>
→ React thinks A→B, B→C, C→remove (3 mutations)
With key: <li key="a">A</li><li key="b">B</li><li key="c">C</li>
→ remove A, keep B and C (1 mutation)
→ Give up "exact" + fit common usage patterns = fast dev experience.Importance of the Key Prop
Common anti-pattern:
{items.map((item, i) => <Card key={i} data={item} />)}
↑
index as key
Problem: on reorder, key=0 always points to the first item.
items [A, B, C] → [B, C] (A removed):
key=0: was A → now B (re-rendered with B's data)
key=1: was B → now C
key=2: was C → unmounted
→ All three Cards' props changed → re-render
(only C's position actually unmounted)
Correct pattern:
{items.map(item => <Card key={item.id} data={item} />)}
↑
unique stable id
→ React knows precisely: A removed, B and C unchanged → B and C
keep the same Card instance.
Especially important for stateful components like form inputs.React Fiber — Interruptible Scheduler (2017+)
React 16's new reconciliation engine.
Old (Stack Reconciler):
Once render starts, runs synchronously to the end
→ Large-tree render holds the main thread → ignores user input (jank)
Fiber:
Render is split into "interruptible" units
→ Main thread can yield for other work (animation, input)
→ Priority system (high: user input / low: data update)
→ Foundation for React 18 "concurrent rendering"
Properties:
- One render can span multiple frames
- Urgent updates (input) preempt lower-priority updates (data)
- Foundation for Suspense / startTransition / useDeferredValue
→ The "60fps even on large pages" promise of modern React.Vue — Tracked Dependency Patch
Vue's reactivity takes a different approach:
const state = reactive({count: 0});
console.log(state.count); // get → Vue tracks the dependency
function Counter() {
return <div>{state.count}</div>; // dep registered
}
state.count++; // set → only components depending on it re-render
// (others unchanged)
→ React diffs the whole tree; Vue patches exactly what changed.
Different runtime cost — Vue is fine-grained with tracking overhead.
Vue 3's VDOM is also smaller than React's + Proxy-based reactivity is
more efficient.Svelte / Solid — No VDOM (Signals)
Another path: remove VDOM entirely.
Svelte:
<script>
let count = 0;
</script>
<button on:click={() => count++}>{count}</button>
→ At compile time, generates exact DOM commands like "update that
button's textContent when count changes".
→ No runtime VDOM diff; mutate DOM directly.
→ Very small bundle (framework runtime near zero).
Solid:
function Counter() {
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}
→ JSX-based but internally uses signals — fine-grained reactivity like Vue.
→ Familiar to React users; performance close to Svelte's.
Trade-off:
- React/Vue: larger framework runtime, stable mental model
- Svelte/Solid: tiny runtime, new paradigmPerformance — Is VDOM Always Slower?
Common myth: "VDOM is expensive → direct DOM is faster"
In reality:
- Small updates: direct DOM ≈ VDOM (both = 1 mutation)
- Large updates: VDOM can batch + apply once → sometimes faster than direct
- Complex reorder: nearly impossible directly (manual diff needed)
→ "The VDOM cost" itself is small (tens of μs). Real cost is what
happens inside the render function.
VDOM's value:
1. Developer experience (declarative)
2. Automatic batching + scheduling
3. Cross-platform (React Native — VDOM mapped to native widgets)
4. Testing / DevTools (tree snapshots)Common Pitfalls
- index as key — wrong instance reuse on reorder / add / remove → state leakage, perf loss.
- Inline objects / functions as props — fresh references per render → memoized children become useless. Use useCallback / useMemo.
- Large components rendering often — even Fiber can't split a single component's render. Break them up.
- Overusing useMemo / useCallback — memoization has cost. Measure first.
- Only worrying about VDOM cost — real bottlenecks are usually big state / big lists / poor component split.
Wrap-up
VDOM at its core — "diff the desired (JS object) against actual DOM". React's heuristics (different type → different subtree, key for reorder) take O(n³) → O(n). Fiber keeps 60fps with interruptible rendering.
Practical — React/Vue VDOM is plenty fast on modern hardware. For smaller bundle / runtime, the no-VDOM signal approach of Svelte / Solid. Either way, correct key prop usage + small components give you 80% of performance.