Static Hoisting(静的巻き上げ)
Static Hoisting とは
Static Hoisting は,テンプレートコンパイル時の最適化テクニックの一つです.テンプレート内の静的な(リアクティブな依存関係を持たない)ノードを検出し,レンダー関数の外部に「巻き上げ」(hoist)することで,再レンダリング時のパフォーマンスを向上させます.

JavaScript の「変数の巻き上げ(hoisting)」と同じ発想です. レンダー関数の中にある静的なコードを,関数の外に「持ち上げる」ことで, 関数が呼ばれるたびに再生成する必要がなくなります!
最適化の効果
- VNode 生成のスキップ: 静的なノードは初回のみ生成され,再利用される
- メモリ使用量の削減: 同一の VNode オブジェクトを再利用
- パッチ処理のスキップ: 静的ノードは比較対象から除外可能
最適化前後の比較
テンプレート
<template>
<div>
<h1>Hello World</h1>
<p>{{ message }}</p>
</div>
</template>最適化なしのコンパイル結果
function render() {
return h('div', null, [
h('h1', null, 'Hello World'), // 毎回生成
h('p', null, message.value)
])
}Static Hoisting 適用後
const _hoisted_1 = h('h1', null, 'Hello World') // 外部で一度だけ生成
function render() {
return h('div', null, [
_hoisted_1, // 参照を再利用
h('p', null, message.value)
])
}
毎回 VNode を生成していたのが,一度生成した VNode を使い回すだけに. ヘッダーやフッターなど,変わらない部分が多いほど効果絶大です!
実装の概要
ConstantTypes
ノードの静的性を表す列挙型です.
export const enum ConstantTypes {
NOT_CONSTANT = 0, // 動的(巻き上げ不可)
CAN_SKIP_PATCH = 1, // パッチ処理をスキップ可能
CAN_HOIST = 2, // 巻き上げ可能
CAN_STRINGIFY = 3, // 文字列化可能(さらに最適化可能)
}hoistStatic 関数
変換フェーズの後で呼び出され,静的ノードを検出して巻き上げます.
export function hoistStatic(root: RootNode, context: TransformContext): void {
walk(root, context, new Map());
}walk 関数
AST を再帰的に走査し,巻き上げ可能なノードを検出します.
function walk(
node: RootNode | TemplateChildNode,
context: TransformContext,
resultCache: Map<TemplateChildNode, ConstantTypes>,
): void {
const { children } = node as RootNode | ElementNode;
if (!children) return;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === 0 // プレーン要素のみ対象
) {
const constantType = getConstantType(child, context, resultCache);
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) {
// 巻き上げ可能
const codegenNode = child.codegenNode as VNodeCall | undefined;
if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {
codegenNode.isStatic = true;
context.hoists.push(codegenNode);
// codegenNode を巻き上げ参照に置き換え
child.codegenNode = context.hoist(codegenNode) as VNodeCall;
}
}
} else {
// 動的な場合は子を再帰的にチェック
walk(child, context, resultCache);
}
}
}
}ポイント:
- プレーン要素(コンポーネントではない)のみを対象
- 静的なノードは
context.hoistsに追加 - 元の
codegenNodeを_hoisted_Nへの参照に置き換え - 動的なノードは子ノードを再帰的にチェック
getConstantType 関数
ノードが静的かどうかを判定します.
export function getConstantType(
node: TemplateChildNode,
context: TransformContext,
resultCache: Map<TemplateChildNode, ConstantTypes>,
): ConstantTypes {
// キャッシュをチェック
const cached = resultCache.get(node);
if (cached !== undefined) {
return cached;
}
if (node.type === NodeTypes.ELEMENT) {
// コンポーネントは巻き上げ不可
if (node.tagType !== 0) {
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}
const element = node as PlainElementNode;
const codegenNode = element.codegenNode;
if (!codegenNode || codegenNode.type !== NodeTypes.VNODE_CALL) {
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}
// 動的な props がないかチェック
if (codegenNode.props) {
const propsType = codegenNode.props.type;
if (propsType !== NodeTypes.JS_OBJECT_EXPRESSION) {
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}
const properties = codegenNode.props.properties;
for (let i = 0; i < properties.length; i++) {
const { key, value } = properties[i];
// キーと値が両方静的でなければ不可
if (key.type !== NodeTypes.SIMPLE_EXPRESSION || !key.isStatic) {
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}
if (value.type !== NodeTypes.SIMPLE_EXPRESSION || !value.isStatic) {
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}
}
}
// 子要素も再帰的にチェック
if (element.children) {
for (let i = 0; i < element.children.length; i++) {
const child = element.children[i];
const childType = getConstantType(child, context, resultCache);
if (childType === ConstantTypes.NOT_CONSTANT) {
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}
}
}
// ディレクティブがあれば不可
if (element.props && element.props.length > 0) {
for (const prop of element.props) {
if (prop.type === NodeTypes.DIRECTIVE) {
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}
}
}
resultCache.set(node, ConstantTypes.CAN_HOIST);
return ConstantTypes.CAN_HOIST;
}
// テキストノードは巻き上げ可能
if (node.type === NodeTypes.TEXT) {
resultCache.set(node, ConstantTypes.CAN_STRINGIFY);
return ConstantTypes.CAN_STRINGIFY;
}
// 補間({{ }})は動的
if (node.type === NodeTypes.INTERPOLATION) {
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}
resultCache.set(node, ConstantTypes.NOT_CONSTANT);
return ConstantTypes.NOT_CONSTANT;
}判定ロジック:
- コンポーネント: 常に動的(props や slots が変わる可能性)
- 動的 props: バインディング(
:class,:styleなど)があれば動的 - ディレクティブ:
v-if,v-forなどがあれば動的 - 補間式:
は動的 - 子要素: 一つでも動的な子があれば親も動的
- 静的テキスト/属性: 巻き上げ可能
コード生成
function genHoists(
hoists: (TemplateChildNode | ExpressionNode)[],
context: CodegenContext
) {
const { push, newline } = context;
for (let i = 0; i < hoists.length; i++) {
const exp = hoists[i];
if (exp) {
push(`const _hoisted_${i + 1} = `);
genNode(exp, context);
newline();
}
}
}hoists 配列に蓄積されたノードを,レンダー関数の前に定数として生成します.
TransformContext の hoist メソッド
hoist(exp) {
context.hoists.push(exp);
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
false,
);
return identifier;
}元のノードを hoists 配列に追加し,_hoisted_N という識別子を返します.これがレンダー関数内で参照されます.
巻き上げ可能な例
<template>
<!-- ✅ 巻き上げ可能 -->
<div class="static">Static content</div>
<img src="/logo.png" alt="Logo">
<p>Fixed text</p>
<!-- ❌ 巻き上げ不可 -->
<div :class="dynamicClass">Dynamic</div>
<p>{{ message }}</p>
<div v-if="show">Conditional</div>
<MyComponent />
</template>transform フェーズでの呼び出し
export function transform(root: RootNode, options: TransformOptions): void {
const context = createTransformContext(root, options);
traverseNode(root, context);
// hoistStatic オプションが有効な場合に実行
if (options.hoistStatic) {
hoistStatic(root, context);
}
createRootCodegen(root, context);
root.components = [...context.components];
root.helpers = new Set([...context.helpers.keys()]);
root.hoists = context.hoists;
}オプション
export interface TransformOptions {
hoistStatic?: boolean; // 静的巻き上げを有効化
// ...
}生成されるコード例
入力テンプレート:
<template>
<div>
<header>
<h1>My App</h1>
<nav>
<a href="/home">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<p>{{ content }}</p>
</main>
</div>
</template>生成コード:
import { createVNode as _createVNode, toDisplayString as _toDisplayString } from 'vue'
// 静的ノードは外部に巻き上げ
const _hoisted_1 = _createVNode("header", null, [
_createVNode("h1", null, "My App"),
_createVNode("nav", null, [
_createVNode("a", { href: "/home" }, "Home"),
_createVNode("a", { href: "/about" }, "About")
])
])
function render(_ctx) {
return _createVNode("div", null, [
_hoisted_1, // 参照を再利用
_createVNode("main", null, [
_createVNode("p", null, _toDisplayString(_ctx.content)) // 動的部分
])
])
}まとめ
Static Hoisting の実装は以下の要素で構成されています:
- ConstantTypes: ノードの静的性レベルを表す列挙型
- getConstantType: ノードが静的かどうかを判定
- walk: AST を走査して巻き上げ可能なノードを検出
- hoist: ノードを巻き上げ配列に追加し参照を返す
- genHoists: 巻き上げられたノードをコード生成
この最適化により,大きなテンプレートで多くの静的コンテンツがある場合に,再レンダリングのパフォーマンスが大幅に向上します.特に,ヘッダー,フッター,サイドバーなど変更されない UI 部分で効果的です.

コンパイラが「この部分は変わらないな」と判断して自動的に最適化してくれます. テンプレートベースのフレームワークならではの強みですね!
ここまでのソースコード: chibivue (GitHub)
