Vue.js 82 🟢 Virtual DOM Architecture, VNode Structure, and Patching Algorithm
Vue’s virtual DOM is not a pure runtime diffing engine. It is a hybrid system in which the template compiler and the runtime renderer cooperate. The compiler analyzes the template, identifies which parts are static and which are dynamic, and emits optimization hints that the renderer uses to skip unnecessary work . The VNode—a plain JavaScript object representing a DOM element—carries these hints in two bitmask fields: shapeFlag encodes the VNode’s type and its children’s type, and patchFlag encodes which specific properties are dynamic . The patching algorithm reads these flags and updates only what the compiler marked as changeable.
This chapter covers the VNode structure, the two bitmask fields that drive optimization, the block tree that collects dynamic descendants, and the keyed diff algorithm that minimizes DOM moves during list reordering.
Key point: A VNode without a patchFlag is treated as static. The renderer does not diff its props or children. This is the central optimization: the compiler’s static analysis tells the runtime what it can skip.
Why Vue’s VDOM is compiler-informed
The brute-force problem in pure runtime VDOM. In React’s model, the reconciler cannot assume anything about the new tree. It must traverse every VNode, compare every prop, and check every child to guarantee correctness. Even a subtree that has never changed produces new VNodes on every render, creating memory pressure and diffing work that produces no DOM updates . Vue’s compiler avoids this by analyzing the template at build time and marking the parts that can actually change.
The two-dimensional optimization. Vue 3’s performance improvement comes from two cooperating mechanisms. The Block Tree collects all dynamic descendants into a flat array (dynamicChildren), so the renderer can skip the static tree structure and iterate only over nodes that might need updating. The PatchFlags on each dynamic node specify which property changed, so the renderer can skip the property comparison for everything else . The result is that a component with a thousand static nodes and one dynamic text interpolation diffs one node and one property, not a thousand nodes.
The structure instability problem. A flat dynamicChildren array only works if the new and old arrays have a one-to-one correspondence. A v-if that changes which branches render breaks that correspondence: the old array might contain a node that the new array does not, and a linear patch would compare mismatched VNodes . The compiler solves this by treating structure-changing constructs (v-if, v-for) as new Blocks, which collect their own dynamic children. The parent’s dynamicChildren contains the block node, and the block node contains its own children. The renderer recurses into the block only when its structure is stable.
a. VNode Structure and ShapeFlags
A VNode is a JavaScript object with fields for the element type, props, children, the DOM element reference, and the optimization bitmasks. The shapeFlag is computed at creation time from the type field: a string type produces ELEMENT, an object produces STATEFUL_COMPONENT, a function produces FUNCTIONAL_COMPONENT . The children type is also encoded: array children, text children, or slots children.
// Conceptual VNode structure
const vnode = {
type: 'div', // element tag, component, or Fragment
props: { class: 'box' }, // attributes and props
children: [ /* vnodes */ ],
el: null, // real DOM node reference (set after mount)
shapeFlag: 1 | 16, // ELEMENT | ARRAY_CHILDREN (bitmask)
patchFlag: 1, // TEXT (dynamic text content)
dynamicChildren: null, // flat array of dynamic descendants (Block only)
}
The shapeFlag bitmask allows the renderer to determine what a VNode is with a single bitwise AND. shapeFlag & ShapeFlags.ELEMENT tells the renderer to mount a DOM element; shapeFlag & ShapeFlags.STATEFUL_COMPONENT tells it to mount a component instance . This is faster than a chain of typeof checks or instanceof comparisons, and it matters because the renderer makes this check for every VNode it encounters.
b. PatchFlags and the Block Tree
The patchFlag is an integer bitmask generated by the compiler. Each bit corresponds to a category of dynamic content: TEXT = 1, CLASS = 2, STYLE = 4, PROPS = 8, and so on . A VNode with patchFlag = 3 (TEXT | CLASS) has dynamic text content and a dynamic class binding. The renderer checks patchFlag & PatchFlags.TEXT and updates the text only if the bit is set.
// Compiler output for <div :class="active">{{ msg }}</div>
// class is dynamic, text is dynamic
const vnode = createVNode('div', { class: active }, msg, 3 /* CLASS | TEXT */);
The renderer’s patchElement function does not compare all props. It checks the flag bits and performs targeted updates. If the STYLE bit is not set, the style comparison is skipped entirely . If the PROPS bit is not set, the props diff is skipped. The flag is the compiler’s promise that everything not marked is static.
The Block Tree extends this optimization across the tree. When the compiler marks a VNode as a Block (the root of a template or a structural directive), that VNode’s dynamicChildren array collects every dynamic descendant, regardless of nesting depth. The renderer iterates this flat array instead of walking the tree structure . A static header, a static sidebar, and a static footer are not in the array. Only the dynamic text, the dynamic class, and the dynamic list items are.
c. The Keyed Diff Algorithm
When a list of keyed children changes, the renderer cannot simply patch index by index. If the first item is removed, every subsequent index shifts, and index-based patching would compare the wrong VNodes. The keyed diff algorithm solves this by matching old and new children by their key attribute.
The algorithm proceeds in four passes. First, it walks from the start of both lists, patching nodes with matching keys until a mismatch. Second, it walks from the end of both lists, patching matching keys until a mismatch . After these two passes, if all old nodes have been consumed, the remaining new nodes are mounted. If all new nodes have been consumed, the remaining old nodes are unmounted.
The third pass handles the case where some old nodes remain unmatched and some new nodes remain unmounted. The algorithm builds a map from the old children’s keys to their indices. It iterates the remaining new children, looking up each key in the map. If a match is found, the old VNode is patched and moved to the correct position if necessary. If no match is found, the new VNode is mounted. A separate longest-increasing-subsequence calculation determines which matched nodes do not need to be moved, minimizing DOM moves .
// Conceptual keyed diff: building the key-to-index map
const keyToNewIndexMap = new Map();
for (let i = s2; i <= e2; i++) {
const child = c2[i];
keyToNewIndexMap.set(child.key, i);
}
Without keys, the renderer falls back to patchUnkeyedChildren, which patches the common prefix by index and mounts or unmounts the remainder. This is why v-for without :key can produce incorrect DOM reuse when the list order changes: the algorithm has no way to know which old node corresponds to which new node, so it reuses by position rather than by identity .
Complete Example Session
// ============================================
// PART 1: A STATIC TEMPLATE
// ============================================
// <div><h1>Title</h1><p>Static content</p></div>
// The compiler marks no dynamic content.
// VNodes have patchFlag = 0 (or undefined).
// The renderer skips diffing on update.
// ============================================
// PART 2: A DYNAMIC TEXT INTERPOLATION
// ============================================
// <p>{{ message }}</p>
// Compiler emits: createVNode('p', null, message, 1 /* TEXT */)
// The renderer updates only textContent, not props or children structure.
// ============================================
// PART 3: DYNAMIC CLASS AND TEXT
// ============================================
// <div :class="active">{{ msg }}</div>
// Compiler emits: createVNode('div', { class: active }, msg, 3 /* CLASS | TEXT */)
// patchElement checks: flag & CLASS → update class
// flag & TEXT → update text
// Skips: style, other props
// ============================================
// PART 4: SHAPEFLAG FOR TYPE CHECK
// ============================================
// const vnode = createVNode('div', null, null)
// vnode.shapeFlag & ShapeFlags.ELEMENT → true
// The renderer knows to call mountElement, not mountComponent.
// ============================================
// PART 5: THE BLOCK TREE
// ============================================
// Template with static header + dynamic list + static footer
// The Block VNode's dynamicChildren = [listVNode, textVNode]
// Renderer iterates dynamicChildren, skipping header and footer.
// ============================================
// PART 6: STRUCTURE CHANGE WITH v-if
// ============================================
// <div v-if="show">{{ a }}</div><div v-else>{{ b }}</div>
// The v-if branch becomes a Block with its own dynamicChildren.
// The parent's dynamicChildren contains the block node, not the inner nodes.
// Renderer patches the block node, which patches its own children.
// ============================================
// PART 7: KEYED DIFF — START AND END PASSES
// ============================================
// Old: [A, B, C, D] New: [A, B, E, D]
// Pass 1 (start): A matches, B matches. C vs E: mismatch, stop.
// Pass 2 (end): D matches. C vs E: mismatch, stop.
// Remaining old: [C]. Remaining new: [E].
// Unmount C, mount E.
// ============================================
// PART 8: KEYED DIFF — MAP LOOKUP
// ============================================
// Old: [A, B, C] New: [C, A, B]
// Start pass: A vs C mismatch.
// End pass: C vs B mismatch.
// Build map: { A:0, B:1, C:2 }
// Iterate new: C → found at old index 2, patch, move to front.
// A → found at old index 0, patch, no move needed.
// B → found at old index 1, patch, no move needed.
// ============================================
// PART 9: UNKEYED DIFF
// ============================================
// Old: [A, B, C] New: [X, Y]
// patchUnkeyedChildren patches A↔X, B↔Y.
// Unmounts C.
// The nodes are reused by position, not identity.
// ============================================
// PART 10: NO PATCHFLAG = STATIC
// ============================================
// A VNode with patchFlag = 0 (or undefined)
// is treated as static by patchElement.
// The renderer returns immediately without diffing.
The ten parts trace the optimization from a fully static template through a dynamic text node, a block tree, and the keyed diff. The critical distinction is between Part 1 (static, no diff) and Part 2 (dynamic text, targeted update). The keyed diff in Parts 7 and 8 shows why keys are required for correct list updates.
Quick Reference
VNode Bitmask Fields
| Field | Purpose | Example Values |
|---|---|---|
shapeFlag | VNode type and children type | ELEMENT, STATEFUL_COMPONENT |
patchFlag | Which properties are dynamic | TEXT=1, CLASS=2, STYLE=4 |
Common PatchFlags
| Flag | Value | Meaning |
|---|---|---|
TEXT | 1 | Dynamic text content |
CLASS | 2 | Dynamic class binding |
STYLE | 4 | Dynamic style binding |
PROPS | 8 | Dynamic non-class/style props |
FULL_PROPS | 16 | Dynamic prop keys |
KEYED_FRAGMENT | 128 | Keyed v-for children |
UNKEYED_FRAGMENT | 256 | Unkeyed v-for children |
Diff Algorithm Phases
| Phase | Action |
|---|---|
| Start pass | Patch matching prefix by key |
| End pass | Patch matching suffix by key |
| Map lookup | Match remaining new keys to old indices |
| LIS | Minimize DOM moves for matched nodes |
Keyed vs. Unkeyed
| Aspect | Keyed | Unkeyed |
|---|---|---|
| Matching | By key attribute | By index |
| Reuse | Correct identity | Positional |
| DOM moves | Minimized by LIS | Not optimized |
Best Practices
✅ Do This:
// Use :key in v-for for stable identity
<li v-for="item in items" :key="item.id">{{ item.name }}</li> // ✅
// Let the compiler emit patchFlags; do not hand-write VNodes unless necessary
// Template compilation produces optimal flags automatically // ✅
❌ Don’t Do This:
// Use array index as key when list order can change
<li v-for="(item, i) in items" :key="i"> // ❌
// Mutate a VNode's patchFlag at runtime
// The renderer trusts the compiler's flags // ❌
// Assume unkeyed lists diff correctly on reorder
// patchUnkeyedChildren reuses by position // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| List reorder produces wrong DOM | Unkeyed or index-keyed v-for | Use stable unique :key |
| Component re-renders but DOM unchanged | PatchFlag marks dynamic, but value is same | Accept the diff; it is cheap |
| VNode created without shapeFlag | Manual createVNode missing type | Use compiler output or h() |
| Block tree mismatch | Structure change inside a Block | Compiler wraps v-if/v-for in new Blocks |
Real-World Examples
1. Static Template VNode
createVNode('div', null, [createVNode('h1', null, 'Title')])
// patchFlag undefined → static
2. Dynamic Text VNode
createVNode('p', null, msg, 1 /* TEXT */)
3. Dynamic Class and Text
createVNode('div', { class: active }, msg, 3 /* CLASS | TEXT */)
4. Block with Dynamic Children
// Compiler sets dynamicChildren to flat array of dynamic descendants
5. Keyed List
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
6. Keyed Diff Start Pass
// Patch while key and type match from the start
7. Keyed Diff Map Lookup
// Build map from old keys to indices
8. Longest Increasing Subsequence
// Determine which matched nodes need not move
9. Unkeyed Diff
// patchUnkeyedChildren reuses by index
10. PatchFlag Check in patchElement
if (patchFlag & PatchFlags.TEXT) { /* update text */ }
Visual
VNode Structure with Bitmasks
┌──────────────────────────────────────────────────────────────┐
│ VNODE │
│ │
│ type: 'div' │
│ props: { class: 'active' } │
│ children: 'Hello' │
│ el: <div> │
│ │
│ shapeFlag: 1 | 16 (ELEMENT | ARRAY_CHILDREN) │
│ patchFlag: 1 | 2 (TEXT | CLASS) │
│ │
│ Renderer reads patchFlag: │
│ ┌─────────────────────────────────────────┐ │
│ │ 3 & TEXT (1) = 1 → update text │ │
│ │ 3 & CLASS (2) = 2 → update class │ │
│ │ 3 & STYLE (4) = 0 → skip style │ │
│ │ 3 & PROPS (8) = 0 → skip other props │ │
│ └─────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Block Tree Collection
┌──────────────────────────────────────────────────────────────┐
│ TEMPLATE (Block root) │
│ │
│ <header>static</header> ← not in dynamicChildren │
│ <main> │
│ <p>{{ msg }}</p> ← in dynamicChildren │
│ <div v-if="show">...</div> ← Block node in dynamicChildren│
│ </main> │
│ <footer>static</footer> ← not in dynamicChildren │
│ │
│ root.dynamicChildren = [pVNode, ifBlockVNode] │
│ │
│ Renderer iterates dynamicChildren: │
│ patch(pVNode) → updates text │
│ patch(ifBlockVNode) → recurses into block's dynamicChildren │
│ │
│ Static header and footer are never visited. │
│ │
└──────────────────────────────────────────────────────────────┘
Keyed Diff Phases
┌──────────────────────────────────────────────────────────────┐
│ OLD: [A, B, C, D, E] │
│ NEW: [A, B, F, D, E] │
│ │
│ PASS 1 (start): A=A ✓, B=B ✓, C vs F ✗ → stop │
│ PASS 2 (end): E=E ✓, D=D ✓, C vs F ✗ → stop │
│ │
│ Remaining old: [C] │
│ Remaining new: [F] │
│ │
│ MAP: { C: 2 } │
│ F not found → mount F at position before D │
│ C not consumed → unmount C │
│ │
│ Result: [A, B, F, D, E] │
│ A, B, D, E are patched in place. C is removed. F is new. │
│ │
└──────────────────────────────────────────────────────────────┘
Keyed Reorder with LIS
┌──────────────────────────────────────────────────────────────┐
│ OLD: [A, B, C] │
│ NEW: [C, A, B] │
│ │
│ Map: { A:0, B:1, C:2 } │
│ │
│ Iterate NEW: │
│ C → old index 2 → move to front │
│ A → old index 0 → already in relative order │
│ B → old index 1 → already in relative order │
│ │
│ LIS of old indices [2, 0, 1] → [0, 1] │
│ A and B are in LIS → no move needed. │
│ C is not in LIS → move C. │
│ │
│ One DOM move instead of three. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| VNode | Plain object with type, props, children, el, bitmasks |
| shapeFlag | Encodes VNode type and children type |
| patchFlag | Encodes which properties are dynamic |
| Block Tree | Flat dynamicChildren array of dynamic descendants |
| Keyed diff | Start pass, end pass, map lookup, LIS |
| Unkeyed diff | Positional patching, no identity reuse |
| No patchFlag | Static VNode; renderer skips diff |
Key takeaways:
- The compiler is part of the rendering system. Vue’s VDOM optimization is not purely runtime. The template compiler analyzes static structure and emits
patchFlaghints that tell the renderer what can change. A VNode without apatchFlagis treated as static and skipped entirely . shapeFlagandpatchFlagare bitmasks for O(1) checks.shapeFlaganswers “what is this VNode?” with a single AND.patchFlaganswers “what about it is dynamic?” with a single AND per category. This avoids string comparisons and property enumeration in the hot path .- The Block Tree flattens the dynamic tree. Instead of recursing through static wrappers to find dynamic nodes, the renderer iterates a flat array of dynamic descendants. Structural directives (
v-if,v-for) become nested Blocks, which preserve the one-to-one correspondence that the flat array requires . - Keys are mandatory for correct list diffing. The keyed diff matches old and new children by
key, performs start and end passes, then uses a map and longest-increasing-subsequence to minimize DOM moves. Unkeyed diff reuses by index, which produces incorrect DOM reuse when order changes . - The diff is targeted, not brute-force. A component with a hundred static props and one dynamic class has a
patchFlagofCLASS. The renderer updates the class and skips the hundred static props. This is the practical consequence of compiler-informed VDOM.
Remember: Vue’s virtual DOM is not a generic diffing engine applied to arbitrary trees. It is a compiler-guided update system. The compiler knows the template, so it knows which nodes can change and which properties on those nodes are dynamic. It emits patchFlag and shapeFlag bitmasks that the renderer reads with single bitwise operations. The Block Tree skips static structure. The keyed diff minimizes DOM moves when lists reorder. Each layer exists because the previous layer’s cost would otherwise be paid on every update. The VNode is the data structure that carries the compiler’s knowledge from build time to runtime.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!