skip to content

Reading the compiled output of a Vue 3.5 template that is mostly static markup, what do _cache[0], -1 /* CACHED */ and createStaticVNode tell you?

level: middleimportance: nice to knowfreq 22%

answer

  1. created once per instance
  2. identical vnode means skip
  3. long static runs become a string
  4. 20 nodes or 5 with attributes
  5. 3.5 moved hoisting to caching

basics

~20 s

Static subtrees are created once and stored in the instance's _cache, marked -1 CACHED, so later renders reuse them and the renderer skips them. Long static runs are stringified into one createStaticVNode HTML string mounted via innerHTML.

solid answer

~50 s

`_cache[0] || (_cache[0] = ...)` means the subtree is built on the first render and stored in the component instance's cache; every re-render returns the same vnode, and the renderer returns immediately when old and new are identical. `-1 /* CACHED */` is the marker flag for such static vnodes. `createStaticVNode("<...>", n)` means a run of consecutive static siblings was stringified into one HTML string, inserted via `innerHTML`, with `n` DOM nodes for hydration. The build-time compiler does this when a run reaches 20 nodes or 5 elements with attributes, never in slot content or in the browser compiler. On update, only nodes with a positive patch flag (plus any components) are visited, here just the `1 /* TEXT */` interpolation. Since Vue 3.5 static nodes are cached per instance instead of hoisted to module scope.

code

vue · 17 lines
vue
<script setup lang="ts">
defineProps<{ price: string }>()
</script>

<template>
  <div class="page">
    <header class="top"><h1>Pricing</h1><p>Plans for every team</p></header>
    <ul class="features">
      <li class="f">Unlimited projects</li>
      <li class="f">Priority support</li>
      <li class="f">Audit log</li>
      <li class="f">Single sign-on</li>
      <li class="f">Custom roles</li>
    </ul>
    <p class="price">{{ price }}</p>
  </div>
</template>

go deeper

for a junior

Recall that static markup is created once and reused, so it is not re-created or compared on updates.

for a middle

Read _cache expressions, the CACHED flag and createStaticVNode, and explain the stringification thresholds and the n argument.

for a senior

Use compiled output to confirm which nodes stay dynamic on a hot component, and trace why an expected-static node was not cached.

for a principal

Decide when compiled-output inspection belongs in a performance review, and when static markup volume simply does not matter.

## The template Consider a marketing-style component that is almost entirely static, with one interpolation: ```html <div class="page"> <header class="top"><h1>Pricing</h1><p>Plans for every team</p></header> <ul class="features"> <li class="f">Unlimited projects</li> <li class="f">Priority support</li> <li class="f">Audit log</li> <li class="f">Single sign-on</li> <li class="f">Custom roles</li> </ul> <p class="price">{{ price }}</p> </div> ``` Paste it into the Vue Template Explorer or the SFC Playground's JS tab and the output (abridged) looks like this: ```js const _hoisted_1 = { class: "page" } const _hoisted_2 = { class: "price" } return (_openBlock(), _createElementBlock("div", _hoisted_1, [ _cache[0] || (_cache[0] = _createStaticVNode("<header class=\"top\">...</ul>", 2)), _createElementVNode("p", _hoisted_2, _toDisplayString(_ctx.price), 1 /* TEXT */) ])) ``` ## Reading it piece by piece 1. **`_hoisted_1`, `_hoisted_2`**: the root `<div>` and the price `<p>` are not static nodes, but their **props objects** are, so those objects are hoisted to module-level constants and reused. 2. **`_openBlock()` / `_createElementBlock("div", ...)`**: the root is a block that collects the dynamic descendants. A single root element is never cached itself, because a parent may pass fallthrough attributes onto it. 3. **`_cache[0] || (_cache[0] = ...)`**: static content is created on the first render and stored in the component instance's `_cache` array. Every later render returns the same object, and the renderer returns immediately when the old and new vnodes are identical, so this subtree is never re-created or diffed. 4. **`_createStaticVNode("<header...>...</ul>", 2)`**: the header and the list were not just cached but **stringified**: turned into one HTML string. At mount, the runtime inserts it via `innerHTML` in one step instead of creating a dozen nodes one call at a time. The second argument, `2`, is the number of top-level DOM nodes in the chunk, which hydration uses to know how many nodes to adopt. 5. **`1 /* TEXT */`** on the price paragraph: the only thing that can ever change here is its text. On update, the block's flat list of dynamic descendants contains just this `<p>`, and the renderer sets its text if it differs. If a static element were not part of a long run, you would see `_createElementVNode(..., -1 /* CACHED */)` inside a `_cache[n]` expression instead of a static string. ## When stringification kicks in The compiler stringifies a run of **consecutive** cached siblings only when the run is big enough to be worth it: - at least **20 nodes** in total (descendants count), **or** - at least **5 elements that carry attributes**. The example qualifies through the second rule: the `header` and the five `li` elements carry a `class`. Other rules apply too: - Stringification is done only when compiling in Node.js, that is, by the build step; the in-browser compiler of the full build does not stringify. - It is skipped for slot content, for `v-once` elements and for attributes that cannot be safely stringified. - Anything the compiler cannot prove constant, such as a component, a dynamic binding or a `ref`, ends the run. ## Static props on dynamic elements When an element is dynamic but its *props object* is fully static, for example `<div id="foo">{{ msg }}</div>`, the element is not cached, but its props are hoisted to a module-level constant such as `const _hoisted_1 = { id: "foo" }` and reused on each render. ## What changed in Vue 3.5 | | Before 3.5 | Vue 3.5 | |---|---|---| | Static vnodes | hoisted to module-scope constants shared by all instances | cached per component instance in `_cache` | | Marker flag | a negative flag for hoisted content | `-1 /* CACHED */` | | Static props objects | hoisted `_hoisted_N` constants | still hoisted `_hoisted_N` constants | The per-instance cache is why the output reads `_cache[0] || (...)` rather than a bare `_hoisted_1` reference for whole nodes. ## Checking your own components 1. Open the SFC Playground's JS tab (or your build's output with minification off) and search for `_cache[` and `createStaticVNode`. 2. For each element you expected to be static but that has a positive flag, find the binding that made it dynamic. 3. Remember that the same template compiled by the full build in the browser shows `_cache` entries but no `createStaticVNode` strings. ## What to take from the reading - Static markup costs nothing after the first render: it is neither recreated nor diffed. - The number of dynamic nodes, not the size of the template, drives update cost. - If a node you expected to be static is not cached, look for what made it dynamic: a binding, a component, a directive or a `ref`.

  • Why is the root <div> in the example not cached, even though its attributes are static?
    The root element may receive fallthrough attributes from the parent, such as a class or listener, so it cannot be treated as constant. The compiler deliberately skips caching a single-element root and makes it the root block instead, while still caching its static children.
  • When would the same static markup not be stringified?
    When the run of consecutive static siblings stays under both thresholds (fewer than 20 nodes and fewer than 5 elements with attributes), when it sits inside slot content, when an element uses `v-once`, or when the template is compiled in the browser by the full build. The nodes are then still cached individually with `-1 /* CACHED */`.

saying these in an interview costs you the question

  • Thinks cached static vnodes are still diffed on every update
  • Believes the full build's in-browser compiler also stringifies static runs
  • Reads createStaticVNode as a dynamic node with a static flag
  • Assumes static caching makes the whole component skip rendering
  • Says Vue 3.5 still hoists whole static vnodes to module scope