π₯ Vue Compiler Pipeline (clean version for review)
π‘ Core idea of Vue 3
Vue compiles a
templateinto arender()function, which at runtime creates VNodes, and then the renderer turns them into DOM via patch.
π§ The full chain
TEMPLATE (string)
β
PARSE
β
Template AST
β
TRANSFORM
β
Optimized AST (directives resolved + patch flags)
β
CODEGEN
β
render function (JS code)
β
RUNTIME EXECUTION
β
render(ctx)
β
h() / createVNode()
β
VNode tree
β
patch()
β
Real DOMβ οΈ THE KEY SPLIT (most important thing to remember)
π‘ COMPILER (build time)
Runs before the application executes (usually via a bundler plugin β @vue/compiler-sfc, vue-loader, unplugin-vue).
What it does:
- parse β AST
- transform β optimize the AST
- codegen β generate JS code
π Result: a JS string (the render function source)
Vue ships three compiler packages, each with a different job:
compiler-coreβ platform-agnostic parse/transform/codegen logiccompiler-domβ adds DOM-specific transforms (v-html,v-modelon inputs, event modifiers,v-show)compiler-sfcβ compiles.vueSingle File Components (splits<template>,<script>/<script setup>,<style>, handles scoped CSS and<script setup>macros likedefineProps/defineEmits)
π΅ RUNTIME (browser time)
Runs while the app is executing.
What it does:
- calls
render() - calls
h()/createVNode() - creates VNodes
- calls
patch()
π Result: DOM
π§ IMPORTANT CORRECTION (about h)
Common mistake:
β Wrong:
h()runs afterrender
β Correct:
h()runs insiderender, during its execution
render(ctx)
β
h()
β
VNodeπ§ 1. PARSE
π turns the template string into an AST
<div>{{ msg }}</div>β
AST tree (Element / Text / Interpolation)The parser is a small state machine that walks the template character by character and produces nodes such as ELEMENT, TEXT, INTERPOLATION, COMMENT, and ATTRIBUTE/DIRECTIVE.
π§ 2. TRANSFORM (the βsmartestβ stage)
π turns the βHTML ASTβ into a βJS-ready ASTβ
What it does:
β v-if
β conditional expression (condition ? vnode1 : vnode2)
β v-for
β renderList()
β v-model
β props + event bindings (e.g. :value + @input, or :modelValue + @update:modelValue for components)
β static nodes
β hoisting
β patchFlags
β dynamic-content markers
π‘ Core idea of transform:
it converts βtemplate structureβ into βfuture JS-code structureβ
Transforms run as a set of node transform and directive transform plugins (transformElement, transformText, vOn, vBind, vFor, vIf, β¦), each visiting the AST and mutating/annotating nodes (this is conceptually similar to a Babel plugin pipeline).
π§ 3. CODEGEN
π AST β JS render function
return function render(_ctx) {
return createElementVNode('div', null, _ctx.msg)
}β‘ Important detail
π there is no VNode yet at this stage
There is only:
JS code that will create a VNode later
π΅ 4. RUNTIME: render()
render(ctx)π executes just like a regular JS function
π₯ 5. h() / createVNode()
h('div', ctx.msg)π creates:
VNode = { type, props, children }h() is just a thin, developer-friendly wrapper around createVNode() β it normalizes arguments (children/props can be omitted or reordered) before calling createVNode directly.
π₯ IMPORTANT
A VNode is created ONLY at runtime
π· 6. PATCH (the diffing engine)
old VNode
vs
new VNodeπ result:
- update text
- change props
- add/remove DOM nodes
- move nodes (v-for + key)
The diff algorithm patches children with a two-ended (head/tail) comparison, then falls back to a longest increasing subsequence (LIS) algorithm to minimize DOM moves for keyed lists β this is what makes :key on v-for so important.
π§ Optimizations (very important, worth knowing precisely)
β Hoisting
const _hoisted_1 = createVNode(...)π static nodes are not recreated on every render β theyβre created once, outside the render function, and reused by reference.
β Patch Flags
π binary flags attached to a VNode telling the runtime exactly what can change, so diffing can skip everything else:
| Flag | Meaning |
|---|---|
TEXT | dynamic text content |
CLASS | dynamic class binding |
STYLE | dynamic style binding |
PROPS | dynamic props (with a known, static key list) |
FULL_PROPS | dynamic props with dynamic keys β full diff needed |
HYDRATE_EVENTS | has event listeners that need hydration |
STABLE_FRAGMENT | fragment whose children order doesnβt change |
KEYED_FRAGMENT | fragment with keyed children (e.g. v-for with :key) |
UNKEYED_FRAGMENT | fragment with unkeyed children |
NEED_PATCH | non-prop patches needed (e.g. refs, hooks) |
DYNAMIC_SLOTS | slots content can change dynamically |
BAIL | optimization gave up β do a full diff |
π they let the runtime skip the parts of the diff that provably cannot have changed.
β Block Tree
π the runtime only tracks dynamicChildren
not the whole DOM tree,
only "what could possibly change"A block is a VNode that collects all of its descendant dynamic VNodes into a flat dynamicChildren array during creation (openBlock() / closeBlock()). At patch time, Vue iterates that flat array directly instead of walking the full tree recursively β this is the core trick behind Vue 3βs speedup over Vue 2βs full-tree virtual DOM diff.
β οΈ IMPORTANT CLARIFICATION ABOUT dynamicChildren
π Vue does not βignore the static tree during diffingβ
It:
does not compare static nodes at all β theyβre skipped entirely, not just cheaply diffed
π₯ Vueβs short formula
template
β AST
β optimized AST
β render function
β VNode tree
β patch
β DOMπ§ The single most important insight (Vueβs core)
Vue = 2 worlds
π‘ Compiler
βI turn a template into codeβ
π΅ Runtime
βI execute code and update the UIβ
π₯ Final insight
A VNode is not an intermediate step between compiler and DOM β a VNode is the runtimeβs format for describing UI.
π Extra: Vue 2 vs Vue 3 compiler, briefly
- Vue 2βs virtual DOM diff walks and compares the entire tree on every update β thereβs no compile-time knowledge of whatβs static vs dynamic.
- Vue 3βs compiler does static analysis ahead of time (hoisting + patch flags + block tree), so the runtime diff only ever touches nodes that are provably capable of changing. This is why Vue 3 can be faster than Vue 2 even though both use a virtual DOM.
<script setup>(compiled bycompiler-sfc) additionally lets the template compiler resolve bindings at compile time (inlining them as_ctx.foovs$setup.foovs plain identifiers), which avoids proxy lookups through the render context for many bindings.
vue Reactivity β where the compiler hands off the baton vue