Understanding Virtual DOM and Diff Algorithm in Vue.js (Vue2 and Vue3)
This article explains the concept of virtual DOM, its structure, and how Vue's diff (patch) algorithm works, including detailed code walkthroughs, optimization strategies, key usage, and differences between Vue2 and Vue3 implementations, helping readers fully grasp the underlying principles and performance improvements.
Because the Diff algorithm computes the differences of the virtual DOM, we first introduce a bit of virtual DOM, understand its structure, and then gradually unveil the Diff algorithm, making it easy to completely understand the principle of Diff.
Understanding Virtual DOM
Virtual DOM is simply a JS object that simulates the DOM structure .
How does it simulate the DOM with a JS object? See the example:
<template>
<div id="app" class="container">
<h1>MuHua</h1>
</div>
</template>The above template is transformed into the following virtual DOM representation:
{
'div',
props: { id: 'app', class: 'container' },
children: [
{ tag: 'h1', children: 'MuHua' }
]
}This structure is called a virtual DOM (Virtual Node, abbreviated vnode ).
A vnode expresses each tag as an object with three possible properties: tag , props , and children :
tag : required, the element name, component, or function.
props : optional, the element's attributes and methods.
children : optional, the content or child nodes. If it is a string, it is a text node without children.
Why use virtual DOM? See the diagram below.
The native DOM has many properties and events; even creating an empty div incurs a cost. Virtual DOM improves performance by using the diff algorithm to compare the new virtual DOM with the previous one, calculate the minimal changes, and update only the changed parts of the real DOM.
In Vue, the template compilation process converts the template into virtual DOM as described above.
Vue updates the virtual DOM using an asynchronous update queue called patch , which compares the new and old vnodes.
Understanding the Diff Algorithm
In Vue, the Diff algorithm is called patch . Its core references Snabbdom: it compares new and old virtual DOMs (the patch process) to find the smallest set of changes for DOM operations.
In Vue1 there was no patch . Each dependency had its own Watcher, which became a performance bottleneck as the project grew. Vue2 introduced a single Watcher per component, and patch was created to precisely locate changed parts.
When does it run?
During the first page render, patch is called once to create the initial vnode.
When component data changes, the setter triggers a Watcher , which calls render to get a new vnode, then patch compares it with the old vnode, calculates the minimal changes, and updates the real DOM.
How does it calculate changes? The algorithm traverses the old and new virtual DOM trees, compares nodes at the same level, and reorders based on differences. A naïve O(n³) approach is avoided by using depth‑first, same‑level comparison strategies.
Diff Algorithm Optimizations
1. Compare only nodes on the same level
2. Compare tag names
3. Compare keys
If the tag name and key are the same, the nodes are considered identical, and the algorithm skips deeper comparison. This is why v-for requires a unique key and why using the array index as a key is discouraged.
Key Usage
When inserting an element into a list, using a proper key ensures only the new element is rendered, while other items remain unchanged.
The key helps the diff algorithm locate the exact node to update, making the patch process efficient.
Vue checks both element type and key to decide if two nodes are the same.
Using a non‑unique key (e.g., array index) can cause unnecessary re‑rendering and bugs.
If the key is missing, Vue may treat all following nodes as changed.
Source reference: src\core\vdom\patch.js -35 line sameVnode()
Diff Algorithm Core Principle – Source Code
The patch function receives four parameters, mainly the old vnode and the new vnode:
function isUndef(v) { return v === undefined || v === null }
function isDef(v) { return v !== undefined && v !== null }
return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
// ... (omitted for brevity) ...
}Key helper functions:
oldVnode : the previous virtual DOM node.
vnode : the new virtual DOM node.
hydrating : used for server‑side rendering.
removeOnly : used by transition-group .
The main flow: If vnode is missing, destroy the old vnode. If oldVnode is missing, create the new vnode. If both exist, use sameVnode to decide whether they represent the same element. If they are the same, call patchVnode for deeper comparison. Otherwise replace the old vnode with the new one.
sameVnode
function sameVnode (a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}patchVnode
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
if (oldVnode === vnode) return
const elm = vnode.elm = oldVnode.elm
// ... (omitted for brevity) ...
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
// post‑patch hook
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}updateChildren
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
// ... (omitted for brevity) ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// four comparison cases + fallback
}
// add remaining new nodes or remove remaining old nodes
}Vue3 Optimizations
Vue3 rewrites the diff algorithm and introduces several performance improvements:
Event caching : event handlers are cached and reused, turning them into static nodes.
Static flags : nodes carry patch‑flag constants (e.g., TEXT, CLASS, STYLE) to skip unnecessary work.
Static hoisting : static sub‑trees are created once and reused across renders.
Longest Increasing Subsequence (LIS) : patchKeyedChildren uses LIS to minimize DOM moves when reconciling keyed lists.
Event Caching Example
<button @click="handleClick">Button</button>Compiled output (Vue3):
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args))
}, "Button"))
}Static Flags
export const enum PatchFlags {
TEXT = 1,
CLASS = 1 << 1,
STYLE = 1 << 2,
PROPS = 1 << 3,
FULL_PROPS = 1 << 4,
HYDRATE_EVENTS = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
HOISTED = -1,
BAIL = -2
}Static nodes are marked with -1 (HOISTED) and are skipped during patch.
Static Hoisting Example
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "MuHua", -1 /* HOISTED */)
export function render(_ctx) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [_hoisted_2, _createElementVNode("p", null, _toDisplayString(_ctx.age), 1 /* TEXT */)]))
}patchKeyedChildren (LIS)
Vue3 replaces the four‑case comparison of Vue2 with a head‑to‑head and tail‑to‑tail check, then uses the longest increasing subsequence to move, add, or delete nodes efficiently.
By keeping the longest increasing subsequence, Vue minimizes DOM moves, achieving the best possible performance.
Previous Highlights
Vue3’s 7 component communication methods vs Vue2’s 12.
What’s new in Vue 3.2.
Advanced JavaScript topics.
22 high‑frequency JavaScript snippets.
Frontend exception monitoring and disaster recovery.
Conclusion
If this article helped you even a little, please give it a like to support the author. Thank you!
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.