skip to content

In Vue 3, what runtime hints does a compiled template give the renderer that a hand-written h() render function does not?

level: juniorimportance: should knowfreq 34%

answer

  1. compiler-informed virtual DOM
  2. flags on dynamic vnodes
  3. static nodes reused, not diffed
  4. blocks track dynamic descendants
  5. h() means full diff

basics

~20 s

A compiled template adds patch flags, cached static vnodes and blocks with a flat list of dynamic descendants, so updates touch only what can change. h() output has none of these, so Vue fully diffs its props and children on each update.

solid answer

~40 s

Vue's compiler analyzes templates and leaves hints for the runtime, which the docs call a compiler-informed virtual DOM. Each dynamic vnode gets a **patch flag** such as `1 /* TEXT */` or `2 /* CLASS */`; static subtrees are **cached** in `_cache` with `-1 /* CACHED */` and skipped because the same vnode comes back; **blocks** (`openBlock()` plus `createElementBlock()`) collect dynamic descendants into a flat `dynamicChildren` array; long static runs become one `createStaticVNode` string; inline handlers are cached. A hand-written `h()` tree has patch flag 0 and no blocks, so the renderer falls back to unoptimized mode: full props diff and full recursive children diff. It is still correct, just more work per update.

code

ts · 13 lines
ts
// Compiled from: <div><h1>Title</h1><p :class="{ active }">{{ msg }}</p></div>
// (abridged; exact identifiers vary with the compile mode)
function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock('div', null, [
    _cache[0] || (_cache[0] = _createElementVNode('h1', null, 'Title', -1 /* CACHED */)),
    _createElementVNode('p', { class: _normalizeClass({ active: _ctx.active }) },
      _toDisplayString(_ctx.msg), 3 /* TEXT, CLASS */)
  ]))
}

// Hand-written equivalent: no flags, no block, no cache
const renderByHand = () =>
  h('div', [h('h1', 'Title'), h('p', { class: { active: active.value } }, msg.value)])

go deeper

for a junior

Recall that Vue's compiler adds hints such as patch flags and cached static nodes, and that h() output has none of them.

for a middle

Explain the unoptimized fallback for h() trees: full props diff and full recursive children diff, with fresh vnodes for static markup.

for a senior

Judge when a hot render-function component should move static markup into a template-based child, and back the decision with profiling.

for a principal

Set guidance on templates versus render functions for a component library, using the compiler's hints as one input alongside readability.

## Why Vue calls its virtual DOM "compiler-informed" A purely runtime virtual DOM cannot assume anything about the tree it receives: on every update it has to walk every node and compare every prop, even for parts that can never change. Vue controls both its **template compiler** and its **runtime renderer**, so the compiler can analyze a template ahead of time and leave **hints** in the generated render function. The Vue docs call this approach the **compiler-informed virtual DOM**. A hand-written render function built with `h()` is ordinary JavaScript the compiler never sees, so it carries none of those hints. ## The hints a compiled template carries | Hint | What the compiler emits | What the renderer does with it | |---|---|---| | **Patch flags** | a number on each dynamic vnode, such as `1 /* TEXT */` or `2 /* CLASS */` | updates only the flagged aspect, such as the text or the class | | **Cached static nodes** | `_cache[n] || (_cache[n] = ...)` with `-1 /* CACHED */` | receives the identical vnode each render and skips it entirely | | **Blocks** | `openBlock()` / `createElementBlock()` calls | collects dynamic descendants into a flat `dynamicChildren` array | | **Static chunks** | `createStaticVNode("<...html...>", n)` | mounts a large static run in one step via `innerHTML` | | **Cached handlers** | inline handlers stored in `_cache` | keeps listener identity stable, so a new arrow function does not look like a changed prop | ## What happens to an `h()` tree instead A vnode created by `h()` has **no patch flag** (it is `0`), and a hand-written render function does not open a block, so the root vnode has **no `dynamicChildren`**. The renderer decides whether to use its optimized path from exactly that: with no block information it runs in **unoptimized mode**, which means: 1. **Full props diff** for every element: every old and new prop key is compared, because nothing says which ones can change. 2. **Full children diff**: children are compared node by node, recursively, including subtrees that are in fact static. 3. **No skipping of static parts**: every render creates fresh vnodes for static markup, which then have to be compared again. The result is still **correct**. Hand-written render functions are a fully supported way to write Vue components; they simply leave more work to the runtime on each update. ## Why this is a Vue-specific answer - The flags are Vue's own `PatchFlags` bitmask, and the renderer checks them with bitwise operations, which are extremely cheap. - Static caching changed form in **Vue 3.5**: static vnodes used to be hoisted to module-level constants and are now cached per component instance in `_cache`. - The compiler also knows which elements need their children diffed by key (fragments from `v-for` carry `KEYED_FRAGMENT` or `UNKEYED_FRAGMENT`), and which multi-root fragments can never reorder (`STABLE_FRAGMENT`). - Vue's docs recommend templates by default for exactly this reason: their deterministic syntax is easy to analyze statically. Render functions are recommended for reusable components with highly dynamic rendering logic. ## How to see the difference yourself 1. Paste a template into the Vue Template Explorer or the SFC Playground's JS tab and look for the numeric flags, the `_cache` expressions and the `openBlock()` calls. 2. Write the same markup with `h()` in a `setup()` render function: the vnodes it produces carry no flag, and the root has no `dynamicChildren`. 3. In a profiler, compare update time for a large, mostly static tree written both ways. The template version touches only the flagged nodes; the `h()` version walks and compares the whole tree. ## Practical takeaways - Keep large, mostly static, frequently re-rendered markup in templates. - If a render-function component turns out to be hot, move its static markup into a template-based child, which brings the hints back for that part of the tree. - Measure before rewriting: for small components the difference between optimized and unoptimized patching is rarely visible.

  • Is a component written with h() ever faster than its template version?
    Not because of patching: without patch flags or blocks the renderer compares every prop and child on each update. A render function can still be the better choice when the output is driven by complex logic that a template would express awkwardly, and for small trees the patching difference is rarely measurable.
  • Why does the renderer skip a cached static vnode so cheaply?
    The cached vnode is stored in the component instance's `_cache` array, so every render returns the very same object. When patching, the renderer first checks whether the old and new vnodes are identical, and if they are it returns immediately, without comparing props or children.

A compiled template is like a form where the clerk has highlighted the few fields that ever change; an h() tree is the same form with no highlights, so every field has to be re-read on each visit.

saying these in an interview costs you the question

  • Claims h() render functions produce patch flags automatically
  • Believes hand-written render functions skip the diff entirely
  • Thinks templates and h() produce identical update work
  • Says render functions are unsupported or deprecated in Vue 3
  • Assumes static markup inside h() calls is detected at runtime and cached