はじめに
Vue 3.6で実験的に追加されたVapor Modeで何が変わるのか、どのようなメリットがあるのかを理解するために調査した内容をまとめました。
筆者は前職でReactを主に使用しており、現在の会社でVueを初めて触ります。
Vapor Modeとは?
Vapor Modeは仮想DOMを使用せず、直接DOMを操作する新しいレンダリングモードです。
従来のVueは仮想DOMを用いて差分を抽出して、DOMを更新していました。仮想DOMツリーの作成と差分比較にCPUサイクルとメモリが必要になるため、ランタイムのオーバーヘッドが発生します。
この課題を解決するため、Vapor Modeでは仮想DOMを介さず直接DOMを更新する仕組みを採用しています。
ランタイムのオーバーヘッドとは?
ランタイムのオーバーヘッドとは、プログラム実行中(ランタイム)に発生する、本来行いたい処理以外の余分な処理コストのことです。
Vueの仮想DOMで表現すると、ボタンを押してカウントを1増やす処理の場合、やりたいことは画面の数字を+1することだけです。
ただ、仮想DOMでは以下の流れになります。
- 新しい仮想DOMツリーをメモリ上に作成
- 古い仮想DOMツリーと差分比較を行う
- 差分から実際のDOM更新命令を組み立てる
- 実DOMを更新する
この工程の中で、オーバーヘッドは1〜3のことを表しています。
Vapor Modeの特徴
Vapor Modeの最大の特徴は、差分検出の作業をなくし、必要なDOM操作コードをコンパイル時に生成することです。
「実行時からコンパイル時に移す」と言われてもあまりピンとこなかったので、以下にまとめます。
従来の仮想DOM方式(実行時に差分検出)
ユーザー操作で状態が変わるたびに、ブラウザ上で上記の1〜4を実行していました。
つまり、「どこが変わったのか」の計算をユーザーのブラウザで毎レンダーごとに行っています。
Vapor Mode(コンパイル時にDOM更新コードを生成)
Vueのコンパイラがビルド時にテンプレートを静的解析し、「どの状態が変わったら、どのDOMをどう更新するか」を直接記述したJavaScriptコードを生成します。
ランタイムではそのコードを実行するだけなので、仮想DOMツリーの作成や差分検出といった処理がそもそも発生しません。
コードでの比較
シンプルなカウンターを例に、コンパイル後のコードを見比べてみます。
元のテンプレート(SFC)
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">
Count: {{ count }}
</button>
</template>
従来モード(仮想DOM)にコンパイルされたイメージ
import { ref, h } from 'vue'
export default {
setup() {
const count = ref(0)
// 状態が変わるたびにこの関数が再実行され、
// 新しい仮想DOMツリーを作成 → 旧ツリーと差分比較 → 実DOMに反映
return () => h(
'button',
{ onClick: () => count.value++ },
`Count: ${count.value}`
)
}
}
Vapor Modeにコンパイルされたイメージ
import { ref, renderEffect, setText, template } from 'vue/vapor'
// テンプレートからDOMを一度だけ作成
const t0 = template('<button>Count: </button>')
export default {
setup() {
const count = ref(0)
const button = t0()
button.addEventListener('click', () => count.value++)
// count が変わったら、このテキストノードだけを直接更新する
renderEffect(() => {
setText(button, `Count: ${count.value}`)
})
return button
}
}
上記コードは概念をわかりやすくするため簡略化しています。
何が違うのか
-
従来モード:
countが変わるたびに() => h(...)全体が再実行され、仮想DOMツリーを作り直して差分比較 -
Vapor Mode:
countが変わると、setTextでそのテキストノードだけを直接更新
差分検出が不要になり、変更箇所への直接的なDOM操作のみで済むのがVapor Modeの大きな特徴です。
パフォーマンスはどのくらい変わっているのか
Vapor Modeで従来のVueと比べてパフォーマンスがどのくらい向上しているのかまとめました。

バンドルサイズの削減
Vue 3.6 alpha時点で、ランタイムのバンドルサイズが**約22KB → 約13KB(-41%)に削減されています。
ロードマップ上は最終的に約10KB(-55%)**まで縮小される見込みです。
バンドルサイズが小さくなることで、初期ロードの高速化につながります。
レンダリング速度の向上
レンダリングのベンチマーク(100 = Vanilla JSの速度を基準)では、以下のスコアになっています。
| スコア | |
|---|---|
| React | 68 |
| Vue Vapor Mode | 90 |
仮想DOMによる差分検出のオーバーヘッドが消えて、Solid.jsと同等の速度になっています。
Vapor Modeの注意点
Vapor Modeを使用するうえで、いくつかの制限があります。
① Composition APIのみ対応
Options APIはサポートされていません。Vapor Modeでは <script setup> を使ったComposition APIでの記述が前提となります。
② getCurrentInstance() が非推奨
Vaporコンポーネント内では null を返すため、コンポーネントインスタンスに依存した処理(VDOM前提のライブラリやプラグイン)は非推奨になっています。
補足:なぜ null を返すのか?
Vapor Modeでは仮想DOMを使用しないため、従来の仮想DOMベースのコンポーネントインスタンスが存在しません。
そのため getCurrentInstance() は参照先がなく、null を返します。
③ 動的コンポーネント(<component :is="...">)の制約
ランタイムでテンプレートをコンパイルするような使い方には制限があります。事前にコンパイル可能な形で記述する必要があります。
<!-- ❌ 文字列でコンポーネントを動的に指定する場合は制限あり -->
<component :is="'MyComponent'" />
<!-- ✅ インポート済みのコンポーネントオブジェクトを渡す場合は問題なし -->
<component :is="MyComponent" />
④ レンダー関数・JSXは非対応
h() 等のレンダー関数(Render Functions)やJSXで構築されたコンポーネントは、Vapor Modeでは動作しません。テンプレート構文での記述が必要です。
⑤ VDOM依存の外部ライブラリ・テストツール
仮想DOMを使用しないため、VDOM前提の外部ライブラリやテストツール(一部のテスティングライブラリ等)は動作しない場合があります。
メリット・デメリット・使い所のまとめ
メリット
- Composition APIの書き方をそのまま使用することができる
- オプトイン方式のため、既存と新規のプロジェクトどちらにも導入することができる
- レンダリング速度がSolid.jsと同等
- バンドルサイズが削減される
-
<script setup vapor>の属性を付け足すだけで切り替えられる - テンプレート構文はそのまま使用することができる
デメリット
- 従来のVDOMで使用していたライブラリなどが対応していない場合がある
- 新しい技術のため、ドキュメントや記事が少ない
- 実験的機能なので、API変更のリスクがある
使い所
向いているケース
- 新規プロジェクト
- 頻繁に情報が更新されるUI(リアルタイム表示、ダッシュボード等)
- 初期表示のスピードを重視するページ(モバイルWebなど)
向いていないケース
- Options API中心のプロジェクト
- JSX・レンダー関数を多用するプロジェクト
- VDOM依存のライブラリを多用しているプロジェクト
Vapor Modeの導入方法
Vapor Modeはオプトイン機能として提供されているため、既存のVueアプリに段階的に導入できます。
導入方法は以下の2パターンがあります。
① 部分的に導入する(既存アプリに混在させる)
従来のVueコンポーネントとVaporコンポーネントを共存させたい場合、VaporInteropPlugin を使います。
既存アプリに少しずつVapor Modeを取り入れたいケースに向いています。
main.js
import { createApp } from 'vue'
import { VaporInteropPlugin } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(VaporInteropPlugin)
app.mount('#app')
Vaporを適用したいコンポーネント(Counter.vue)
<script setup vapor>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
ポイントは <script setup> に vapor 属性を付与することです。これにより、このコンポーネントだけがVapor Modeでコンパイルされます。
② 全体に導入する(アプリ全体をVaporで動かす)
新規プロジェクトや、アプリ全体をVapor Modeで動かしたい場合は createVaporApp を使います。
main.js
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')
全てのコンポーネント
<script setup vapor>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
Vapor ModeはVue 3.6時点で実験的機能です。APIや関数名(
VaporInteropPlugin/createVaporApp等)は今後変更される可能性があります。実装時は公式ドキュメントで最新情報を確認してください。
おわりに
Vapor Modeについて調査した内容をまとめました。
仮想DOMによるランタイムのオーバーヘッドを排除し、コンパイル時にDOM操作コードを直接生成することでパフォーマンスを向上させていることがわかりました。
また、オプトイン方式での提供により、既存のアプリケーションを大きく書き換えることなく段階的に導入できる点も、開発者にとって使いやすくなっていると感じました。
現状は実験的機能ですが、正式リリースされた際には積極的に使用していきたいと思います。