Skip to content

Virtual DOM in Vue — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Virtual Dom in Vue. We cover key concepts, practical examples, and best practices to help you master this topic.

Virtual DOM in Vue uses a reactive dependency system with optimized patching, template-compiled render functions, and fine-grained reactivity for efficient updates.

What You'll Learn

  • How Vue's Virtual DOM differs from React's
  • How Vue's reactivity system interacts with the Virtual DOM
  • How Vue compiles templates to optimized render functions
  • How Vue 3's patch flags improve diffing

Why It Matters

Vue's approach to Virtual DOM combines compile-time optimization with runtime reactivity. Understanding this helps you leverage Vue's performance characteristics and avoid common pitfalls.

flowchart LR
  A[Vue Component] --> B[Template Compiler]
  B --> C[Optimized Render Function]
  C --> D[VNode Tree]
  D --> E[Patch Function]
  E --> F[DOM Updates]
  G[Reactive State] --> H[Trigger Effects]
  H --> C

Vue's Reactivity and the Virtual DOM

Vue tracks dependencies at the property level, not the component level.

// Vue 3 reactivity system
import { ref, reactive, computed, watchEffect } from 'vue';

const count = ref(0);
const user = reactive({ name: 'Alice', age: 30 });

// The render function is wrapped in a watcher
// It re-runs only when its reactive dependencies change
function render() {
    // This creates a VNode tree
    return h('div', [
        h('p', 'Count: ' + count.value),
        h('p', 'Name: ' + user.name),
        h('p', 'Age: ' + user.age)
    ]);
}

// When count.value changes:
// 1. The reactive system notifies subscribers
// 2. The render function re-runs
// 3. A new VNode tree is created
// 4. The patch function diffs old and new trees
// 5. Only changed DOM nodes are updated

// If only count changes, user.name and user.age are not re-evaluated
// Only the count paragraph is updated in the DOM

Template Compilation and Patch Flags

Vue's compiler analyzes templates and adds optimization hints to VNodes.

// Vue template:
// <div>
//   <h1>{{ title }}</h1>
//   <p class="static">Static content</p>
//   <p :class="dynamicClass">{{ dynamicText }}</p>
// </div>

// Compiled render function (simplified):
function render(_ctx, _cache) {
    return h('div', [
        h('h1', _ctx.title),                          // DYNAMIC: text content
        h('p', { class: 'static' }, 'Static content'), // STATIC: never changes
        h('p', {
            class: _ctx.dynamicClass,                  // DYNAMIC: class binding
            patchFlag: 16  // PatchFlags.CLASS
        }, _ctx.dynamicText)                            // DYNAMIC: text content
    ])
}

// Patch flags tell the runtime exactly what to check:
// PatchFlags.TEXT = 1      // Only text content changed
// PatchFlags.CLASS = 16    // Only class changed
// PatchFlags.STYLE = 32    // Only style changed
// PatchFlags.PROPS = 8     // Only props changed

// During patching:
// If patchFlag is 1 (TEXT), only update textContent
// No need to check class, style, or other attributes
// This avoids unnecessary attribute comparisons

Static Hoisting

Vue hoists static VNodes outside the render function, so they are created once and reused.

// Vue template:
// <div>
//   <span class="static">Always the same</span>
//   <span>{{ dynamic }}</span>
// </div>

// Compiled output (simplified):
// Static node is hoisted — created only once
const _hoisted_1 = h('span', { class: 'static' }, 'Always the same');

function render(_ctx, _cache) {
    return h('div', [
        _hoisted_1,  // Reused across renders, no diff needed
        h('span', _ctx.dynamic)  // Only this needs checking
    ]);
}

// Static hoisting benefits:
// 1. Static VNodes are created once
// 2. No diffing needed for hoisted nodes
// 3. Less memory allocation per render
// 4. Faster reconciliation for static parts

Vue 3 Patch Algorithm

Vue's patch algorithm handles different VNode types efficiently.

// Simplified Vue 3 patch function
function patch(oldVNode, newVNode, container) {
    if (oldVNode === newVNode) {
        return;  // Same reference, nothing to do
    }

    if (oldVNode && oldVNode.type !== newVNode.type) {
        // Different types: unmount old, mount new
        unmount(oldVNode);
        oldVNode = null;
    }

    const { type, shapeFlag } = newVNode;

    switch (type) {
        case Text:
            // Text node: update textContent
            processText(oldVNode, newVNode, container);
            break;
        case Comment:
            processComment(oldVNode, newVNode, container);
            break;
        case Fragment:
            // Fragment: patch children only
            processFragment(oldVNode, newVNode, container);
            break;
        default:
            if (shapeFlag & ShapeFlags.ELEMENT) {
                // Regular DOM element
                processElement(oldVNode, newVNode, container);
            } else if (shapeFlag & ShapeFlags.COMPONENT) {
                // Component
                processComponent(oldVNode, newVNode, container);
            }
    }
}

// Element patching with patch flags
function patchElement(oldVNode, newVNode) {
    const el = newVNode.el = oldVNode.el;
    const { patchFlag, dynamicChildren } = newVNode;

    if (patchFlag) {
        // Optimized path: only check what the flag indicates
        if (patchFlag & PatchFlags.CLASS) {
            // Only update className
            hostPatchProp(el, 'class', null, newVNode.props.class);
        }
        if (patchFlag & PatchFlags.STYLE) {
            // Only update style
            hostPatchProp(el, 'style', null, newVNode.props.style);
        }
        if (patchFlag & PatchFlags.TEXT) {
            // Only update textContent
            el.textContent = newVNode.children;
        }
        // No need to check other attributes
    } else {
        // Full diff path (for dynamic templates)
        patchProps(oldVNode.props, newVNode.props, el);
    }

    // Patch children if not optimized
    if (!newVNode.dynamicChildren) {
        patchChildren(oldVNode, newVNode, el);
    }
}

Vue vs React Virtual DOM

Key differences between Vue and React implementations.

// React:
// - Renders entire component tree on any state change
// - Uses Fiber for interruptible rendering
// - Diffing happens at runtime only
// - No compile-time optimizations
// - Concurrent mode for prioritization

// Vue 3:
// - Tracks dependencies at property level
// - Only re-renders components whose dependencies changed
// - Compile-time optimization (patch flags, hoisting)
// - Block tree optimization (dynamic children tracking)
// - No concurrent mode (simpler scheduling)

// Performance characteristics:
// React: More runtime overhead, better for highly dynamic UIs
// Vue: Less runtime overhead due to compile-time optimizations

// Simple benchmark:
function measureReRender() {
    const vueComponent = createVueComponent();
    const reactComponent = createReactComponent();

    // Update a single property
    console.time('Vue update');
    for (let i = 0; i < 1000; i++) {
        vueComponent.count = i;
    }
    console.timeEnd('Vue update');

    console.time('React update');
    for (let i = 0; i < 1000; i++) {
        reactComponent.setCount(i);
    }
    console.timeEnd('React update');
}

Common Mistakes

  1. Assuming Vue re-renders the entire component tree on every state change (it only re-renders components with changed dependencies).
  2. Mutating reactive objects without using reactive() or ref() wrappers, breaking reactivity detection.
  3. Expecting v-for without :key to work efficiently (always provide :key for lists).
  4. Overusing v-if to toggle large templates (v-show is better for frequently toggled elements).
  5. Not using the Composition API for better tree-shaking and type inference.

Practice Questions

  1. How does Vue track reactive dependencies? At the property level using Proxy-based reactivity.
  2. What are patch flags in Vue 3? Compile-time optimization hints that tell the patcher exactly which properties changed.
  3. What is static hoisting in Vue? Moving static VNodes outside the render function so they are created once and reused.
  4. How does Vue 3's block tree optimization work? The compiler tracks dynamic children in an array, skipping static children during diffing.

Challenge

Build a Vue component that demonstrates the compiler's optimization. Create a template with static content, dynamic bindings, and conditional rendering. Use the Vue template explorer to inspect the compiled output and identify patch flags and hoisted nodes.

FAQ

How does Vue's Virtual DOM differ from React's?

Vue uses compile-time optimizations (patch flags, hoisting) and property-level reactivity. React uses runtime-only diffing with Fiber.

Does Vue re-render the entire component tree?

No. Vue tracks dependencies at the property level. Only components whose reactive dependencies changed re-render.

What are patch flags in Vue?

Patch flags are compiler-generated hints on VNodes that tell the patcher exactly which properties changed (text, class, style, props).

What is static hoisting in Vue?

Static hoisting moves VNodes that never change outside the render function, so they are created once and reused across renders.

Is Vue faster than React?

Both are fast for typical use cases. Vue's compile-time optimizations give it an edge for static-heavy templates. React's Fiber enables concurrent features.

Mini Project

Build a Vue 3 performance comparison tool. Create a component with static and dynamic parts. Measure render time with and without compiler optimizations. Display patch flags, hoisted nodes, and the compiled render function. Show the difference in DOM operations between naive and optimized updates.

What's Next

Lesson 12: Virtual DOM vs No Virtual DOM

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro