Vue Vapor Modeとは何か
Vue Vapor Modeを解説。SFCを直接DOM操作に変換する仕組み、Vue 3.6 RCでの変更点、イベントとスロットの注意点を紹介。
Vue Vapor Modeは、Vue単一ファイルコンポーネント向けのコンパイルモードであり、テンプレートを直接的なDOM操作へと変換します。これにより、仮想DOMの生成や差分検出を行わずにレンダリングと更新が実行されるため、ベースラインのバンドルサイズ、更新コスト、メモリ使用量が削減されます。
Vaporはしばらく前からカンファレンスで紹介されてきましたが、Vueのドキュメントにはいまだにこれに関するページがありません。詳細はVue coreのリリースノートに記載されています。
本記事では、Vapor Modeによってコンポーネントの動作がどのように変わるのか、そしてリリースサイクルにおける現在の位置づけを解説します。あわせて、Vaporにおいてサイレントに失敗し、容易に踏み込んでしまう2つの挙動についても取り上げます。
要点
- Vapor ModeはVue SFCを、VNodeの生成と差分検出ではなく直接的なDOM操作へとコンパイルします。バンドルサイズの縮小と更新の高速化はここに由来します。
- Vapor ModeはVue 3.6のリリース候補版で機能完成(feature-complete)していますが、安定版ではありません。3.6系は依然としてプレリリース段階にあり、v3.6.0-rc.9(2026年9月18日)はGitHub上でPre-releaseと表示されている一方、Latestラベルは3.5系のv3.5.43(2026年9月17日)に付いたままです。
- Vaporは
<script setup vapor>、<script vapor>、<template vapor>によってコンポーネント単位でオプトインします。対象はテンプレートのみのSFCとscript setupを使用するSFCに限られ、Options APIはサポートされません。 createVaporApp()は仮想DOMランタイムを一切読み込まない純粋なVaporアプリをマウントします。VDOMコンポーネントをホストするためにvaporInteropPluginをインストールすると、そのランタイムが再び取り込まれ、サイズ面の利点が相殺されます。- VaporコンポーネントにはVNodeもパブリックなインスタンスプロキシも存在しないため、
getCurrentInstance()はnullを返し、app.config.globalPropertiesは適用されず、コンポーネントのテンプレートrefは$el、$props、$attrs、$slots、$refsを公開しなくなります。
Vue Vapor Modeはどのように動作するのか
v3.6.0-rc.1のリリースノートでは、Vaporは単一ファイルコンポーネントをコンパイルする第2の方式として位置づけられており、より小さな初期バンドルとより高いパフォーマンスを狙ったものです。これらはデフォルトでは一切有効になりません。自分でコンポーネント単位で有効化する必要があり、Vaporがカバーする範囲のVue APIはおおむね従来どおりの挙動を示します。記述するソースコードは変わりません。変わるのは出力です。ランタイムが前回のツリーと差分を取るためのVNodeを生成するレンダー関数の代わりに、コンパイラはノードを一度だけ生成し、各リアクティブな依存関係をそれが制御する特定のDOM更新へと直接結びつけるコードを出力します。
仮想DOMを取り除くことで、3つのコストが同時に削減されます。純粋なVaporアプリでは差分検出ランタイム自体を出荷する必要がないため、ベースラインのバンドルが小さくなります。更新時にはVNodeの割り当てとツリー比較がスキップされるため、1つのrefの変更は1つのテキストノードまたは1つの属性のみに作用します。さらに、レンダー間でシャドウツリーを保持しないため、コンポーネントあたりのメモリフットプリントも低下します。
コンポーネント自体は通常のComposition APIコードと変わりません:
<script setup vapor>
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
<template>
<button @click="count++">{{ count }} / {{ doubled }}</button>
</template>
VDOMモードでコンパイルされる同一のコンポーネントとの違いは、vapor属性だけです。
リリース状況: 機能完成、ただし安定版ではない
Vapor Modeは安定版のVueリリースには出荷されていません。vuejs/coreのリリース一覧では、3.6系はいまだリリース候補段階にあります。2026年9月18日に公開されたv3.6.0-rc.9にはPre-releaseラベルが付いており、Latestラベルは2026年9月17日に公開されたv3.5.43に属しています。npmでも同様で、vueパッケージのlatest dist-tagは3.5.43を指しており、リリース候補版は別のrcタグの下に置かれています。3.6.0の安定版タグは存在しません。
事実として言えることはより限定的であり、正式出荷と混同されやすい点でもあります。v3.6.0-rc.1のリリースノートには、Vapor ModeがVue 3.6 RCにおいて機能完成していると記載されており、これこそが3.6系がリリース候補段階へ移行した理由です。機能完成とはスコープを指すものであり、安定性を指すものではありません。Vue自身のリリースポリシーは、すべてのプレリリースを同じように扱っています。すなわち不安定であり、本番で運用するためではなく、そのビルドが自分たちのスタックに適合するかをチームがテストするために存在し、ビルド間で互換性を壊すことも自由である、という扱いです。インストールする場合は、正確なバージョンを固定してください。
Vapor Modeへのオプトイン方法
Vaporはプロジェクト単位ではなく、コンポーネント単位で有効化します。対象となるコンポーネントは2種類です。テンプレートのみを含む単一ファイルコンポーネントと、script setupで記述されたコンポーネントです。Options APIで書かれたコンポーネントはそもそもVaporへコンパイルできません。対象となるコンポーネントをマークする方法は3つあります。完全な形の<script setup vapor>、その省略形である<script vapor>、そしてtemplateタグに付けるvaporマーカーで、これはファイル全体をVaporとしてコンパイルします。
<!-- Form 1: the explicit form -->
<script setup vapor>
// ...
</script>
<!-- Form 2: shorthand for <script setup vapor> -->
<script vapor>
// ...
</script>
<!-- Form 3: marks the whole SFC as Vapor -->
<template vapor>
<!-- ... -->
</template>
実務上の帰結として、監査工程が必要になります。data、methods、mountedを用いて書かれているコンポーネントは、vaporフラグが何らかの効果を発揮する前にscript setupへ変換しておかなければなりません。
VaporコンポーネントとVirtual DOMコンポーネントは混在させられるのか
VaporコンポーネントとVirtual DOMコンポーネントは混在可能であり、アプリをどのようにマウントするかによってバンドルに含まれるものが決まります。すべてのコンポーネントがVaporであれば、createVaporApp()でマウントしてください。この経路では仮想DOMランタイムがビルドから除外され、ベースラインサイズの大幅な削減はここから生まれます。createApp()でマウントされたアプリは、Vaporの子コンポーネントをレンダリングする前にvaporInteropPluginをインストールする必要があります。Vaporアプリ側も同じプラグインをインストールして仮想DOMの子をホストできますが、3.6.0-rc.1のリリースノートが説明しているとおり、そうするとランタイムが戻ってきてサイズ削減のメリットの大半が失われます。
// Pure Vapor: the VDOM runtime is never loaded
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')
// Existing VDOM app hosting Vapor components
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'
createApp(App).use(vaporInteropPlugin).mount('#app')
レンダー関数やJSXで書かれたコンポーネントもVirtual DOMコンポーネントであるため、Vaporアプリ内ではinteropが必要です。ただしinteropは万能ではありません。一方のモードを他方に入れ子にした場合、通常のprops、イベント、スロットは処理されますが、すべてのエッジケースがまだ扱えるわけではなく、仮想DOM上に構築されたコンポーネントライブラリはVapor下で正しく動作しないことがあります。Vueチームの推奨は、アプリの各領域には単一のレンダリングモードを割り当て、モードをまたいだ入れ子は最小限に抑えるというものです。
Vaporコンポーネントで使えなくなるもの
VaporコンポーネントにはVNodeもパブリックなインスタンスプロキシも存在しません。rc.1のリリースノートに挙げられた非サポート項目はすべてこの1つの境界に由来しており、それぞれに具体的な影響があります:
| Vaporで非サポート | 実際に何が壊れるか |
|---|---|
| Options API | data、methods、ライフサイクルオプションを使用するコンポーネントはそもそもVaporへコンパイルできません。 |
app.config.globalProperties | プラグインが注入したグローバルはVaporコンポーネント内に存在しません。必要なものは明示的にinjectしてください。 |
getCurrentInstance() | nullを返すため、内部インスタンスに依存するサードパーティコードはVaporコンポーネント内で失敗します。 |
@vue:xxx要素ライフサイクルイベント | 要素単位のフックは廃止されています。テンプレートrefとonMountedを使用してください。 |
v-memo | 手動メモ化のエスケープハッチは利用できず、削除する必要があります。 |
コンポーネントのテンプレートref上の$el、$props、$attrs、$slots、$refs | 親が子の内部に手を伸ばすパターンが壊れます。契約をpropsとemitsへ移してください。 |
リリースノートは、この一致度についても慎重な書き方をしています。Vaporは仮想DOMと同じように振る舞うことを目指していますが、2つのレンダラーは構造が大きく異なるため、エッジケースにおける小さな不一致は想定内であり、その程度の不一致が破壊的変更として扱われるのは、従来の挙動がドキュメント化されていた場合に限られます。
始める前に知っておくべき2つの落とし穴
委譲イベントとstopPropagation()
rc.1の設計では、委譲可能なイベントはdocumentレベルで処理されます。要素はハンドラーを保持しますが、要素自体には何もバインドされません。documentに置かれた単一のリスナーが処理を担い、イベントがたどる経路を追跡して、その途上で出会ったハンドラーを発火させます。もし祖先要素が伝播の途中でstopPropagation()を呼び出すと、イベントはdocumentに到達せず、そのハンドラーは決して実行されません。委譲をスキップしてリスナーを要素へ直接アタッチする形式は3つあります: @[event]="onClick"、v-bind="{ onClick }"、v-on="{ click: onClick }"です。
<script setup vapor>
const onClick = () => save()
</script>
<template>
<!-- the ancestor stops propagation, so a delegated handler never fires -->
<div @click.stop>
<button @click="onClick">Save</button>
</div>
<!-- binds directly to the element instead -->
<div @click.stop>
<button v-on="{ click: onClick }">Save</button>
</div>
</template>
この失敗モードはサイレントです。例外もログも発生せず、エラー監視は健全なセッションとして報告する一方で、ユーザーは何も起こらないコントロールをクリックし続けます。これを表面化させる手法がセッションリプレイです。クリックの後にDOMの変化が続かない様子を観察でき、これこそハンドラーが一度も実行されなかったことの痕跡だからです。同じことはVapor/VDOMの境界にも当てはまり、そこではinteropの欠落が例外ではなくレンダリングの異常として現れる傾向があります。
RC系全体の変更の大半はハイドレーションとスロットに集中しており、イベントリスナーのマージ方法に対する修正はわずか数件です。したがって、正確なRCバージョンを固定し、rc.1の記述がそのまま当てはまると仮定するのではなく、インストールするバージョンのminorブランチのCHANGELOGを確認してください。
slots.default()はレンダリングする、報告するのではない
Vaporでは、slots.default()の呼び出しはスロットを覗き見る無害な操作ではありません。この呼び出しはスロットのレンダリングコードを実行し、Blockやdomノードを構築し、リアクティブなエフェクトを設定し、ページがハイドレーション中であればサーバーが既に送信したDOMの所有権を取得することもあります。したがって、スロットを呼び出してフォールバックをレンダリングすべきか判断するというVDOMでよくある習慣は、Vaporでは副作用を伴います。
<script setup vapor>
import { useSlots } from 'vue'
const slots = useSlots()
// Wrong: this call renders the slot rather than inspecting it
const showFallback = !slots.default?.()
</script>
判断はテンプレート内で表現し、スロットのレンダリングはテンプレートに委ねてください:
<template>
<slot>Fallback</slot>
</template>
今Vapor Modeを試すべきなのは誰か
リリースノートは、現段階での用途として2つを挙げています。既存アプリの一部、たとえばレンダリング速度が重要な単一ページにVaporを導入すること、そして小規模な新規アプリを最初からVaporで書くことです。その裏返しもはっきり述べておく価値があります。安定版のみを使う依存関係ポリシーを持つプロジェクト、VDOMコンポーネントライブラリの上に構築された画面、そしていまだOptions APIのコードベースは、いずれも最初の候補としては不適切です。1つ目はそもそもプレリリースをインストールできず、残りの2つはinteropと非サポート項目の影響を最も強く受ける位置にあるからです。
Vapor Modeを見据えた計画
Vapor Modeは、スケジュールを組んで進める移行作業ではなく、今日1つの画面で評価できるコンパイラの変更として扱ってください。正確なリリース候補版を固定し、リストやアニメーションを多用するページを1つ<script setup vapor>へ変換し、安定版3.6を前提とした計画を立てる前にvuejs/coreのリリース一覧を確認してください。
FAQ
Vapor ModeはNuxtで動作しますか?
はい、実験的機能として、かつVue 3.6以降でのみ動作します。Nuxtはアプリのルートを仮想DOM上に保ったまま、個々のコンポーネントやページをVaporとしてマークできるようにしているため、段階的に採用できます。すべてをVaporで構成したNuxtアプリはまだ実現できず、Vaporコンポーネントを指すテンプレートrefから要素は取得できません。インストールされているVueがそれより古い場合、Nuxtは警告を出してこのオプションを再び無効化します。
Vue 3.6のリアクティビティ性能向上を得るためにVapor Modeは必要ですか?
いいえ。3.6系はVueのリアクティビティコアをalien-signalsの上に再構築しており、それによる速度とメモリ使用量の改善は、有効化の操作を一切必要とせず、そのリリース系列上のすべてのアプリに適用されます。Vapor Modeはそれとは別の、コンポーネント単位のコンパイル方式の変更です。したがって、3.6上で動作する標準的な仮想DOMアプリは、どこでもVaporを有効化することなく、すでにリアクティビティの改善を享受できます。
Vapor Modeはサーバーサイドレンダリングとハイドレーションをサポートしていますか?
SSRハイドレーションは、初期のアルファビルドでは含まれていませんでしたが、Vue 3.6リリース候補版のVapor機能セットに含まれています。ハイドレーションの正しさはプレリリース系列全体で最も変更が多かった領域の1つであるため、正確なリリース候補版を固定し、仮想DOMレンダリングと同等であると仮定せず、実際のページでサーバー出力、ハイドレーション、不一致からの復帰をテストしてください。
サードパーティのコンポーネントライブラリはVaporコンポーネント内で動作しますか?
採用を決める前にテストしてください。レンダー関数やJSXとして出荷されているコンポーネントは仮想DOMコンポーネントのままであり、interopが必要です。またgetCurrentInstanceを呼び出したりglobalPropertiesを読み取ったりするライブラリコードは、Vaporコンポーネント内では何も見つけられません。コンポーネントインスタンスではなくprovideとinjectの上に構築されたものは通常そのまま動作し続けます。Nuxtが自身のコンポーザブルや組み込みコンポーネントの大半はVapor下でも変更不要だとしているのは、このためです。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k