How Virtual DOM Works — Diffing Algorithm
In this tutorial, you will learn about How Virtual Dom Works. We cover key concepts, practical examples, and best practices to help you master this topic.
The Virtual DOM diffing algorithm compares two virtual trees recursively, identifying changes in elements, attributes, and children to produce minimal DOM patches.
What You'll Learn
- How the diffing algorithm works step by step
- How element type comparison determines reuse or replacement
- How attribute changes are detected and applied
- How child reconciliation works at the tree level
Why It Matters
The diff algorithm is the core of Virtual DOM performance. Understanding it helps you write components that diff efficiently and avoid unnecessary re-renders.
flowchart TD
A[Old Virtual Tree] --> C[Difference Engine]
B[New Virtual Tree] --> C
C --> D{Compare node types}
D -->|Same type| E[Update attributes]
D -->|Different type| F[Replace entire subtree]
E --> G[Recurse into children]
G --> H[Generate patch list]
H --> I[Apply patches to real DOM]
Element Type Comparison
The first check in the diff algorithm compares element types. If types differ, the entire subtree is replaced.
function diff(oldNode, newNode) {
// Different types: replace entire node
if (oldNode.type !== newNode.type) {
return { type: 'REPLACE', newNode };
}
// Same type: diff props and children
const patches = [];
// Diff attributes/props
const propPatches = diffProps(oldNode.props, newNode.props);
if (propPatches.length > 0) {
patches.push({ type: 'UPDATE_PROPS', patches: propPatches });
}
// Diff children
const childPatches = diffChildren(oldNode.children, newNode.children);
if (childPatches.length > 0) {
patches.push({ type: 'UPDATE_CHILDREN', patches: childPatches });
}
return { type: 'UPDATE', patches };
}
// Example: different types means full replacement
const oldVNode = { type: 'div', props: {}, children: ['Hello'] };
const newVNode = { type: 'span', props: {}, children: ['Hello'] };
// Result: REPLACE div with span entirely
Attribute Diffing
Attributes are compared by key. Added, removed, and changed attributes are detected.
function diffProps(oldProps, newProps) {
const patches = [];
const allKeys = new Set([
...Object.keys(oldProps || {}),
...Object.keys(newProps || {})
]);
for (const key of allKeys) {
const oldVal = oldProps ? oldProps[key] : undefined;
const newVal = newProps ? newProps[key] : undefined;
if (oldVal === newVal) {
continue; // No change
}
if (newVal === undefined) {
patches.push({ type: 'REMOVE_PROP', key });
} else {
patches.push({ type: 'SET_PROP', key, value: newVal });
}
}
return patches;
}
// Example
const oldProps = { className: 'active', id: 'test', 'data-old': 'value' };
const newProps = { className: 'highlight', id: 'test', style: 'color: red' };
// Results:
// className changed: 'active' -> 'highlight'
// id unchanged: skipped
// data-old removed from props
// style added: 'color: red'
Child Reconciliation
Children are compared position by position. Different frameworks use different strategies.
function diffChildren(oldChildren, newChildren) {
const patches = [];
const maxLen = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLen; i++) {
if (i >= oldChildren.length) {
// New child added
patches.push({ type: 'ADD_CHILD', index: i, node: newChildren[i] });
} else if (i >= newChildren.length) {
// Old child removed
patches.push({ type: 'REMOVE_CHILD', index: i });
} else {
// Compare existing children
const childPatch = diff(oldChildren[i], newChildren[i]);
if (childPatch.patches && childPatch.patches.length > 0) {
patches.push({ type: 'UPDATE_CHILD', index: i, patch: childPatch });
}
}
}
return patches;
}
// Simple positional comparison
const oldChildren = [
{ type: 'li', props: {}, children: ['A'] },
{ type: 'li', props: {}, children: ['B'] },
];
const newChildren = [
{ type: 'li', props: {}, children: ['A'] }, // Same
{ type: 'li', props: {}, children: ['C'] }, // Text changed
{ type: 'li', props: {}, children: ['D'] }, // Added
];
// Without keys, this produces:
// Child 0: no change
// Child 1: text update B -> C
// Child 2: add new li with text D
Applying Patches to the Real DOM
The patch function takes the diff output and applies minimal changes.
function patch(domNode, patches) {
patches.forEach(p => {
switch (p.type) {
case 'REPLACE':
// Create new DOM node and replace
const newNode = createDomNode(p.newNode);
domNode.parentNode.replaceChild(newNode, domNode);
break;
case 'UPDATE_PROPS':
p.patches.forEach(propPatch => {
switch (propPatch.type) {
case 'SET_PROP':
domNode[propPatch.key] = propPatch.value;
break;
case 'REMOVE_PROP':
domNode.removeAttribute(propPatch.key);
break;
}
});
break;
case 'UPDATE_CHILDREN':
const childNodes = Array.from(domNode.childNodes);
p.patches.forEach(childPatch => {
switch (childPatch.type) {
case 'ADD_CHILD':
domNode.appendChild(createDomNode(childPatch.node));
break;
case 'REMOVE_CHILD':
if (childNodes[childPatch.index]) {
domNode.removeChild(childNodes[childPatch.index]);
}
break;
case 'UPDATE_CHILD':
if (childNodes[childPatch.index]) {
patch(childNodes[childPatch.index], [childPatch.patch]);
}
break;
}
});
break;
}
});
}
Common Mistakes
- Assuming keys are optional for simple lists (they improve diff accuracy for reordered lists).
- Expecting the diff to handle every edge case optimally (some operations require O(n) comparisons).
- Comparing complex objects in props diffing (use stable references or memoization).
- Mutating the old virtual tree before diffing (causes incorrect diff results).
- Thinking the diff algorithm only runs once per update (it recurses through the entire tree).
Practice Questions
- What is the first check in the diff algorithm? Element type comparison. Different types cause full replacement.
- How are attributes diffed? By comparing old and new prop objects key by key.
- How are children diffed without keys? Positionally — children at the same index are compared.
- What is a patch? An instruction describing a minimal DOM change (replace, update props, add child, etc.).
Challenge
Build a complete virtual DOM implementation with createElement, diff, and patch functions. Use it to render a counter UI. Verify that clicking the increment button produces only the minimal DOM patch (text content change, not full re-render).
FAQ
Mini Project
Implement a simple virtual DOM engine with three functions: h() to create virtual nodes, diff() to compare two trees, and patch() to apply changes. Test it by rendering a list, changing item text, and verifying that only the changed text node is updated in the real DOM.
What's Next
Lesson 3: Reconciliation Process
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro