Open-Source Wikis

/

Vue.js

/

Features

/

Template compilation

vuejs/core

Template compilation

This page walks through how a Vue template (an .vue <template> block, an in-DOM template, or a template: '...' option) becomes a JavaScript render function that produces vnodes. For the package internals, see compiler-core and compiler-dom. For the SFC entry point, see SFC <script setup>.

The journey

graph TD
    A["template source string"]
    B["tokenizer<br/>compiler-core/src/tokenizer.ts"]
    C["baseParse<br/>compiler-core/src/parser.ts"]
    D["AST"]
    E["transform pipeline<br/>compiler-core/src/transform.ts"]
    F["AST + codegenNode"]
    G["cacheStatic pass<br/>compiler-core/src/transforms/cacheStatic.ts"]
    H["generate<br/>compiler-core/src/codegen.ts"]
    I["render function<br/>(string of JS)"]

    A --> B --> C --> D --> E --> F --> G --> H --> I

The same pipeline runs in three contexts:

  • Build time, via compiler-sfc. The bundler (Vite, vue-loader) calls compileTemplate which routes to compiler-dom.compile (or compiler-ssr.compile for SSR). The output is a JavaScript module imported by the SFC's runtime side.
  • Runtime, via the full vue build. When a component has a template: '...' option and no precompiled render function, registerRuntimeCompiler from packages/vue/src/index.ts invokes compiler-dom.compile and the result becomes instance.render.
  • In the playground / template explorer, where the same compile function is invoked by user-facing tooling for debugging.

Stage 1: tokenize and parse

packages/compiler-core/src/tokenizer.ts is a streaming state machine — ~36KB, modeled on the WHATWG HTML tokenizer with Vue extensions. It emits callbacks for start tags, end tags, attribute names/values, interpolation {{/}}, comments, and CDATA. The parser in packages/compiler-core/src/parser.ts consumes these callbacks and builds a typed AST whose nodes are defined in packages/compiler-core/src/ast.ts.

The parser is permissive in HTML mode: it handles void tags, raw text elements (<script>, <style> are not parsed for Vue syntax in template context), and entity decoding. compiler-dom's parserOptions plugs in browser-flavored helpers — isVoidTag, isPreTag, namespace resolution for SVG/MathML, entity decoding — so the same parser can be reused in compiler-ssr with the same options.

Stage 2: transform

The transform driver (packages/compiler-core/src/transform.ts) walks the AST depth-first. Each visit invokes:

  • All nodeTransforms in order. Each can return an onExit callback that runs after children are visited.
  • All matching directiveTransforms for the element's directives.

compiler-core/src/compile.ts registers the base set:

function getBaseTransformPreset(prefixIdentifiers?: boolean): TransformPreset {
  return [
    [
      transformOnce,
      transformIf,
      transformMemo,
      transformFor,
      ...(__DEV__ ? [transformExpression] : []), // dev-only in non-prefixIdentifiers mode
      transformSlotOutlet,
      transformElement,
      trackSlotScopes,
      transformText,
    ],
    {
      on: transformOn,
      bind: transformBind,
      model: transformModel,
    },
  ];
}

compiler-dom adds transformStyle, transformVHtml, transformVText, transformShow, and overrides transformModel/transformOn with DOM-aware versions. compiler-ssr swaps in ssrTransformElement, ssrTransformComponent, and friends.

Structural directives

v-if, v-else-if, v-else, v-for, v-slot are not regular directiveTransforms — they are StructuralDirectiveTransforms declared via createStructuralDirectiveTransform. They get the chance to short-circuit the normal traversal so they can rewrite the surrounding tree before children are visited:

  • vIf (packages/compiler-core/src/transforms/vIf.ts) collects sibling branches into an IfNode.
  • vFor (packages/compiler-core/src/transforms/vFor.ts) builds a ForNode whose codegen calls _renderList(source, (item, index, key, …) => …).
  • vSlot (packages/compiler-core/src/transforms/vSlot.ts) builds a slots object literal whose entries are _withCtx(...) functions.

transformElement

packages/compiler-core/src/transforms/transformElement.ts is the largest single transform (~28KB). It produces the codegenNode for an element — the call expression that codegen will emit. The work it does:

  1. Resolve the element type. <component is="…"> becomes _resolveDynamicComponent. <MyComp> may resolve to a setup binding, an Options API component, or _resolveComponent("MyComp").
  2. Walk attributes. Static attributes go into a single object literal; v-bind="…", v-on, custom directives, and dynamic keys go into separate args. Patch flags are accumulated.
  3. Pick the helper: _createBlock for blocks, _createElementBlock for native-element blocks, _createVNode/_createElementVNode for non-block vnodes. Whether something becomes a block depends on whether it can change child structure (a v-if or v-for, a fragment, <Suspense>, <Teleport>).
  4. If a directive transform attached runtimeDirectives, wrap the call in _withDirectives.

transformExpression

When prefixIdentifiers: true (the default for ESM-bundler builds and SSR) packages/compiler-core/src/transforms/transformExpression.ts rewrites every free identifier in templates: foo becomes _ctx.foo, someEvent($event) keeps $event but qualifies others. This makes the generated render function pure — it depends only on its arguments and module-scope hoists, never on a with block.

The implementation walks the JS expression with babelUtils.walkIdentifiers, tracking scopes for v-for, v-slot, v-on, and arrow functions. Identifiers that resolve to a binding from <script setup> (provided via BindingMetadata) are qualified specially — setup constants become _unref(foo) if they are refs.

cacheStatic

packages/compiler-core/src/transforms/cacheStatic.ts runs over the post-transform tree. It computes getConstantType for each node, propagating bottom-up: a node is constant if all its props are constant, all its children are constant, and the element itself isn't a component.

Constant subtrees are lifted into module-scope _hoisted_* variables in the generated code:

const _hoisted_1 = /*@__PURE__*/ _createElementVNode(
  'h1',
  null,
  'Static title'
);

export function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock('div', null, [
      _hoisted_1,
      _createElementVNode(
        'p',
        null,
        _toDisplayString(_ctx.dynamic),
        1 /* TEXT */
      ),
    ])
  );
}

compiler-dom replaces this with stringifyStatic (its own transformHoist) outside the browser build: a large constant subtree becomes _createStaticVNode("<h1>Static title</h1>", 1) so the runtime can innerHTML it once instead of constructing many vnodes.

Stage 3: generate

packages/compiler-core/src/codegen.ts walks each codegenNode and produces a string of JavaScript. Decisions made during codegen:

  • Imports. The transform stage records which runtime helpers it needs in a helpers Set on the context. Codegen converts these to either an import statement (module mode) or a const _Vue = Vue destructuring (function mode for runtime compilation). The runtime helper symbols are listed in packages/compiler-core/src/runtimeHelpers.ts.
  • Hoist serialization. Each hoisted vnode/value becomes a top-level const _hoisted_N = … before the render function.
  • Source maps. When the input has source positions (the parser preserves { line, column, offset } on every node), codegen emits a source map alongside the code so dev-tools can map back to the original .vue template.

For runtime compilation in vue.global.js, the result is wrapped via new Function('Vue', code) (or new Function(code) in IIFE builds) and executed against the global Vue namespace.

Mode-specific differences

Mode prefixIdentifiers hoistStatic cacheHandlers Block structure
Runtime compile (in-browser) false (uses with) true true yes
ESM bundler (compiler-sfc → compiler-dom) true true true yes
SSR (compiler-ssr) true false false n/a — string output

The with-block mode in runtime compilation lets the generated code reference template identifiers without qualification. Strict mode/ESM forbids with, so prefix mode is mandatory for module output.

Files to know

  • packages/compiler-core/src/tokenizer.ts
  • packages/compiler-core/src/parser.ts
  • packages/compiler-core/src/transform.ts
  • packages/compiler-core/src/transforms/transformElement.ts
  • packages/compiler-core/src/transforms/cacheStatic.ts
  • packages/compiler-core/src/transforms/transformExpression.ts
  • packages/compiler-core/src/transforms/vIf.ts, vFor.ts, vSlot.ts
  • packages/compiler-core/src/codegen.ts
  • packages/compiler-dom/src/index.ts
  • packages/compiler-dom/src/transforms/stringifyStatic.ts
  • packages/vue/src/index.ts (runtime registration)

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Template compilation – Vue.js wiki | Factory