Skip to content

How Virtual DOM Works — Diffing Algorithm

DodaTech Updated 2026-06-28 5 min read

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

  1. Assuming keys are optional for simple lists (they improve diff accuracy for reordered lists).
  2. Expecting the diff to handle every edge case optimally (some operations require O(n) comparisons).
  3. Comparing complex objects in props diffing (use stable references or memoization).
  4. Mutating the old virtual tree before diffing (causes incorrect diff results).
  5. Thinking the diff algorithm only runs once per update (it recurses through the entire tree).

Practice Questions

  1. What is the first check in the diff algorithm? Element type comparison. Different types cause full replacement.
  2. How are attributes diffed? By comparing old and new prop objects key by key.
  3. How are children diffed without keys? Positionally — children at the same index are compared.
  4. 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

How does the Virtual DOM diff work?

The diff algorithm compares two virtual trees recursively. It checks node types first, then attributes, then children. Only differences produce patches.

What happens when element types differ?

The entire subtree is replaced. The old DOM node and all its children are removed and recreated.

How are attributes diffed efficiently?

By iterating over the union of old and new attribute keys and comparing values. Unchanged keys are skipped.

Why does the diff need keys for lists?

Without keys, the diff compares children positionally. With keys, it can detect reordering and avoid unnecessary DOM operations.

What is a patch in Virtual DOM terms?

A patch is a minimal set of DOM operations (replace, update props, add/remove children) computed by the diff algorithm.

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