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 --> IThe same pipeline runs in three contexts:
- Build time, via
compiler-sfc. The bundler (Vite, vue-loader) callscompileTemplatewhich routes tocompiler-dom.compile(orcompiler-ssr.compilefor SSR). The output is a JavaScript module imported by the SFC's runtime side. - Runtime, via the full
vuebuild. When a component has atemplate: '...'option and no precompiled render function,registerRuntimeCompilerfrompackages/vue/src/index.tsinvokescompiler-dom.compileand the result becomesinstance.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
nodeTransformsin order. Each can return anonExitcallback that runs after children are visited. - All matching
directiveTransformsfor 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 anIfNode.vFor(packages/compiler-core/src/transforms/vFor.ts) builds aForNodewhose codegen calls_renderList(source, (item, index, key, …) => …).vSlot(packages/compiler-core/src/transforms/vSlot.ts) builds aslotsobject 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:
- Resolve the element type.
<component is="…">becomes_resolveDynamicComponent.<MyComp>may resolve to a setup binding, an Options API component, or_resolveComponent("MyComp"). - 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. - Pick the helper:
_createBlockfor blocks,_createElementBlockfor native-element blocks,_createVNode/_createElementVNodefor non-block vnodes. Whether something becomes a block depends on whether it can change child structure (av-iforv-for, a fragment,<Suspense>,<Teleport>). - 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
helpersSet on the context. Codegen converts these to either animportstatement (module mode) or aconst _Vue = Vuedestructuring (function mode for runtime compilation). The runtime helper symbols are listed inpackages/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.vuetemplate.
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.tspackages/compiler-core/src/parser.tspackages/compiler-core/src/transform.tspackages/compiler-core/src/transforms/transformElement.tspackages/compiler-core/src/transforms/cacheStatic.tspackages/compiler-core/src/transforms/transformExpression.tspackages/compiler-core/src/transforms/vIf.ts,vFor.ts,vSlot.tspackages/compiler-core/src/codegen.tspackages/compiler-dom/src/index.tspackages/compiler-dom/src/transforms/stringifyStatic.tspackages/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.