πŸ”₯ Vue Compiler Pipeline (clean version for review)

πŸ’‘ Core idea of Vue 3

Vue compiles a template into a render() 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 logic
  • compiler-dom β€” adds DOM-specific transforms (v-html, v-model on inputs, event modifiers, v-show)
  • compiler-sfc β€” compiles .vue Single File Components (splits <template>, <script>/<script setup>, <style>, handles scoped CSS and <script setup> macros like defineProps/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 after render

βœ… Correct:

h() runs inside render, 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:

FlagMeaning
TEXTdynamic text content
CLASSdynamic class binding
STYLEdynamic style binding
PROPSdynamic props (with a known, static key list)
FULL_PROPSdynamic props with dynamic keys β€” full diff needed
HYDRATE_EVENTShas event listeners that need hydration
STABLE_FRAGMENTfragment whose children order doesn’t change
KEYED_FRAGMENTfragment with keyed children (e.g. v-for with :key)
UNKEYED_FRAGMENTfragment with unkeyed children
NEED_PATCHnon-prop patches needed (e.g. refs, hooks)
DYNAMIC_SLOTSslots content can change dynamically
BAILoptimization 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 by compiler-sfc) additionally lets the template compiler resolve bindings at compile time (inlining them as _ctx.foo vs $setup.foo vs plain identifiers), which avoids proxy lookups through the render context for many bindings.

vue Reactivity β€” where the compiler hands off the baton vue