2026年のVue 3パフォーマンス:Vapor Mode、Alien Signalsと仮想DOMの終焉

Vue 3.6 RC Vapor Modeのパフォーマンスを徹底解説。仮想DOMの排除方法、Alien Signalsリアクティビティシステム、Solid.jsとのベンチマーク比較、本番アプリケーション向け最適化テクニックまで。

Vue 3 Vapor Mode パフォーマンス最適化の図解

Vue 3.6のVapor Modeは、Vue 2で仮想DOMを採用して以来、最も大きなレンダリングアーキテクチャの変革である。Single File Componentsを命令的なDOM操作に直接コンパイルすることで、Vapor Modeは長年Vueのレンダリングパイプラインを定義してきた差分処理のオーバーヘッドを完全に排除する。さらに、Alien Signalsによるリアクティビティの全面書き換えにより、Vue 3.6はSolid.jsやSvelte 5とベンチマーク上で同等の性能に到達した。開発者が新しいAPIを習得する必要はない。

Vapor Modeの概要

Vapor Modeは、Vue 3.6におけるオプトイン方式のコンパイル戦略であり、仮想DOMを完全にバイパスする。Vapor Modeでコンパイルされたコンポーネントは、各リアクティブ依存関係を影響するDOMノードに直接紐付けるため、ツリー走査なしで外科的な更新を行う。有効化は単一の属性で完了する:<script setup vapor>。

Vueの仮想DOMの仕組みとボトルネックになった理由

仮想DOM(VDOM)パターンは、Reactによって普及し、Vue 2で採用された。実際のDOMツリーの軽量なJavaScript表現を生成する仕組みである。状態が変更されるたびに、Vueは新しいVDOMツリーを生成し、以前のツリーとの差分を取り、変更されたノードのみを実際のDOMにパッチする。

このアプローチはほとんどのアプリケーションで十分に機能する。差分アルゴリズムはO(n)時間で実行され、Vueのコンパイラは静的なサブツリーを差分パスから除外する最適化も行っている。しかし、特定のシナリオではオーバーヘッドが蓄積される。

  • 大規模なリスト:数百行のリストでは、更新のたびに完全なサブツリー比較が実行される
  • 頻繁な状態変更:アニメーションやリアルタイムデータでは、VDOMの生成と破棄がGCの負荷を増大させる
  • 深いコンポーネントツリー:ツリー走査とパッチ生成のコストが増大する

Vue 3のテンプレートコンパイラは、静的ホイスティング、パッチフラグ、ブロックツリーなどのVDOM最適化を導入し、不要な処理を削減した。しかし、根本的なアーキテクチャは依然として、レンダリングサイクルごとにJavaScriptオブジェクトの生成・差分・破棄を必要としていた。

Vue 3.6 Vapor Mode:仮想DOMのコンパイル除去

Vapor Modeは根本的に異なるアプローチを取る。テンプレートをVDOMノードを返すレンダー関数にコンパイルする代わりに、VaporコンパイラはDOM要素を直接生成・更新するコードを出力する。各リアクティブバインディングは特定のDOM操作にマッピングされ、中間表現を必要としない。

標準的なVueコンポーネントが各モードでどのようにコンパイルされるかを以下に示す。

vue
<!-- Counter.vue -->
<script setup vapor>
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++
</script>

<template>
  <button @click="increment">
    Count: {{ count }}
  </button>
</template>

従来のVDOMモードでは、このテンプレートは仮想ノードツリーを返すレンダー関数にコンパイルされる。クリックのたびに、Vueは新しいVDOMツリーを作成し、前回のツリーと差分を取り、テキスト内容の変更を検知して実DOMにパッチする。

Vapor Modeでは、コンパイラは以下のようなコードを生成する。

javascript
// Vapor コンパイル出力(簡略化)
const button = document.createElement('button')
const text = document.createTextNode('Count: 0')
button.appendChild(text)

// 直接バインディング:リアクティブソース -> DOM操作
effect(() => {
  text.nodeValue = \`Count: \${count.value}\`
})

button.addEventListener('click', increment)

リアクティブエフェクトがcountをtext.nodeValueに直接接続する。VDOMの生成も、差分処理も、パッチ適用も発生しない。状態変更は正確に1つのDOM操作のみをトリガーする。

プロジェクトでのVapor Mode有効化

Vapor Modeはコンポーネント単位で動作する。2つの統合戦略が存在する。

vaporApp.js — 完全なVaporアプリケーション(VDOMランタイムなし)javascript
import { createVaporApp } from 'vue'
import App from './App.vue'

createVaporApp(App).mount('#app')
hybridApp.js — VDOM + Vaporのハイブリッド構成javascript
import { createApp, vaporInteropPlugin } from 'vue'
import App from './App.vue'

createApp(App)
  .use(vaporInteropPlugin)
  .mount('#app')

ハイブリッドアプローチにより、段階的な移行が可能である。データテーブル、リアルタイムダッシュボード、アニメーション多用のビューなど、パフォーマンスが重要なコンポーネントをVaporに移行しつつ、残りのアプリケーションは標準のVDOMランタイムを使い続けることができる。両方のコンポーネントタイプは同一のコンポーネントツリー内で共存する。

Vue 3.6 RCにおけるVapor Modeの制限事項

Vapor Modeは<script setup>を使用したComposition APIが必須である。Options API、app.config.globalProperties、getCurrentInstance()、v-memo、要素単位のライフサイクルイベント(@vue:mountedなど)はサポートされていない。カスタムディレクティブは、バインディングオブジェクトの代わりにリアクティブゲッターを受け取る異なるインターフェースを使用する。純粋なVaporアプリケーションでは<Suspense>はサポートされないが、VDOMの親コンポーネント内でVaporコンポーネントをラップする場合はSuspenseが動作する。

Vue 3.6 RCにおけるイベント委譲の変更

ベータとRCリリースの間で、VueチームはVapor Modeのイベント処理方法を変更した。ベータではVaporがイベントを自動的にドキュメントレベルに委譲していた。RC.2以降、リスナーはデフォルトで要素に直接アタッチされる。大規模リストでパフォーマンスのためにドキュメントレベルの委譲を使用するには、明示的な.delegate修飾子を使用する。

vue
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <button @click.delegate="selectItem(item)">
        {{ item.name }}
      </button>
    </li>
  </ul>
</template>

この変更により、VaporはstopPropagation()周辺の標準的なVueセマンティクスに沿い、イベント処理に関する開発者の期待に合致するようになった。

Alien Signals:Vueの新しいリアクティビティエンジン

Vue 3.6はVapor Modeと並行して、もう1つの重大な変更を含んでいる。Alien Signalsライブラリに基づく@vue/reactivityの全面的な書き換えである。Johnson Chu氏が開発したAlien Signalsは、メモリ割り当てと計算オーバーヘッドの両方を削減するプッシュプルリアクティビティアルゴリズムを実装している。

プッシュプルモデルは2つのフェーズで動作する。

  1. プッシュフェーズ:シグナル(リアクティブソース)の値が変更されると、すべての依存するcomputed propertyに「dirty」フラグを伝播する。この走査はブーリアンフラグの反転のみであるため、低コストである。
  2. プルフェーズ:computed propertyが読み取られると、dirtyフラグを確認する。dirtyであれば再計算し、クリーンであればキャッシュされた値を即座に返す。

この設計により、不要な再計算が排除される。Vue 3.5のリアクティビティシステムでは、3つのシグナルに依存するcomputed propertyは、いずれかのシグナルが変更されるたびに再計算されていた(computed結果が同じであっても)。Alien Signalsは、値が実際に読み取られ、かつ少なくとも1つの依存関係が変更された場合にのみ再計算を行う。

reactivityComparison.tstypescript
import { ref, computed, watch } from 'vue'

const firstName = ref('Jane')
const lastName = ref('Doe')
const isActive = ref(true)

// このcomputedはdirtyかつ読み取られた時のみ再計算される
const displayName = computed(() => {
  return isActive.value
    ? \`\${firstName.value} \${lastName.value}\`
    : 'Inactive User'
})

// isActiveの変更でdisplayNameはdirtyとマークされる
// しかし、何かがdisplayName.valueを読み取るまで計算は実行されない
isActive.value = false

// ここで初めて再計算が発生する
console.log(displayName.value) // 'Inactive User'

内部データ構造も変更された。Alien SignalsはSetベースの依存関係追跡を双方向連結リストに置き換え、リアクティブ依存関係ごとのメモリを削減し、クリーンアップ時の走査速度を向上させている。

ベンチマーク結果:Alien Signalsの実測値

Vue 3.6 RCベンチマーク数値は、Vue 3.5に対して一貫した改善を示している。

指標Vue 3.5Vue 3.6(Alien Signals)改善率
リアクティブrefあたりのメモリベースライン-14%メモリ割り当て削減
computed再計算ベースライン平均-20%不要な実行の削減
コンポーネントマウント(10万件)約300ms約100ms3倍高速
First Contentful Paintベースライン標準0.7sフレームワークオーバーヘッド削減
ベースバンドルサイズ約16KB10KB未満-37%

これらの改善はすべてのVue 3.6アプリケーションに自動的に適用される。コンポーネント単位でのオプトインが必要なVapor Modeとは異なり、Alien SignalsリアクティビティシステムはVue 3.6のデフォルトである。

Vue.js / Nuxt.jsの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

実践的なVue 3パフォーマンス最適化テクニック

Vapor ModeとAlien Signalsはフレームワークレベルのオーバーヘッドに対処するが、アプリケーションレベルのパフォーマンスはコンポーネントの構造に依存する。以下のテクニックはVDOMとVaporの両方のコンポーネントに適用される。再利用可能なパフォーマンス最適化ロジックの構築については、高度なVue 3 Composablesのガイドを参照。

大規模データ構造に対するShallow Reactivity

ディープリアクティビティはすべてのネストされたプロパティをProxyでラップする。大規模なデータセット(APIレスポンス、設定オブジェクト、キャッシュされた状態)では、数千の不要なProxyラッパーが生成される。shallowRefとshallowReactiveはリアクティビティをトップレベルの参照に限定する。

shallowReactivity.tstypescript
import { shallowRef, triggerRef } from 'vue'

interface Product {
  id: number
  name: string
  variants: { sku: string; price: number }[]
}

// ref自体のみがリアクティブ、ネストされたプロパティは対象外
const products = shallowRef<Product[]>([])

// データ取得 — 新しい配列を割り当ててリアクティビティをトリガー
async function fetchProducts() {
  const response = await fetch('/api/products')
  products.value = await response.json() // 更新をトリガー
}

// ネストデータの変更 — 手動でトリガーが必要
function updatePrice(productId: number, sku: string, newPrice: number) {
  const product = products.value.find(p => p.id === productId)
  const variant = product?.variants.find(v => v.sku === sku)
  if (variant) {
    variant.price = newPrice
    triggerRef(products) // 明示的なトリガーが必要
  }
}

v-memoディレクティブによる高コストリストレンダリングの最適化

VDOMコンポーネントで長いリストをレンダリングする場合、v-memoは依存値に基づいてサブツリーのレンダリング結果をキャッシュする。依存関係が変更されていないメモ化されたサブツリーに対して、VDOMは差分処理を完全にスキップする。v-memoはVapor Modeでは利用できないことに注意。

vue
<!-- ProductGrid.vue -->
<script setup>
import { ref } from 'vue'

const products = ref([])
const selectedId = ref(null)
</script>

<template>
  <div class="grid">
    <div
      v-for="product in products"
      :key="product.id"
      v-memo="[product.id === selectedId, product.price]"
      :class="{ selected: product.id === selectedId }"
    >
      <h3>{{ product.name }}</h3>
      <span>{{ product.price }}</span>
    </div>
  </div>
</template>

v-memoの配列[product.id === selectedId, product.price]はVueに対して「選択状態または価格が変更されない限り、このアイテムの再レンダリングをスキップする」ことを指示する。500件の商品リストで1つだけが選択される場合、VDOMの処理は500件のサブツリー差分から2件(以前選択されていたアイテムと新しく選択されたアイテム)に削減される。

Suspenseを活用した非同期コンポーネントとコード分割

重いコンポーネントの遅延読み込みにより、初期バンドルを軽量に保つことができる。VueのdefineAsyncComponentとSuspenseの組み合わせで、ローディング状態を宣言的に処理する。Suspenseパターンとエッジケースについては、TeleportとSuspenseのガイドを参照。

vue
<!-- Dashboard.vue -->
<script setup>
import { defineAsyncComponent } from 'vue'

// 重いチャートライブラリは必要な時のみ読み込み
const AnalyticsChart = defineAsyncComponent(() =>
  import('./components/AnalyticsChart.vue')
)

const DataExport = defineAsyncComponent({
  loader: () => import('./components/DataExport.vue'),
  delay: 200,       // 200ms後にローディング表示
  timeout: 10000,   // 10秒でタイムアウト
})
</script>

<template>
  <Suspense>
    <template #default>
      <AnalyticsChart />
      <DataExport />
    </template>
    <template #fallback>
      <div class="skeleton-loader" />
    </template>
  </Suspense>
</template>

Vapor Mode vs VDOM:使い分けのガイドライン

Vapor Modeは仮想DOMの普遍的な代替ではない。各コンパイルモードには、異なるコンポーネントプロファイルに適した強みがある。

シナリオ推奨モード理由
データテーブル(1000行以上)Vapor行ごとのVDOMオーバーヘッドを排除
リアルタイムダッシュボードVapor頻繁な更新が直接DOMバインディングの恩恵を受ける
アニメーション多用コンポーネントVaporVDOMチャーンによるGC圧力がない
サードパーティVDOMコンポーネントライブラリVDOM相互運用レイヤーが複雑さを増す
Options APIを使用するコンポーネントVDOMVaporはComposition APIが必須
複雑なバリデーションを伴うフォームどちらでも両モードでレンダリングオーバーヘッドは最小限
静的コンテンツページどちらでもSSG/SSRが重い処理を担当

推奨される移行パスは、まずVue DevToolsのパフォーマンスタブでアプリケーションをプロファイリングすることである。レンダリング時間と再レンダリング頻度が最も高いコンポーネントを特定し、それらをVapor Modeに変換し、影響を測定してから拡大していく。

面接対策:Vue 3パフォーマンスとVapor Mode

以下の質問は、2026年にVueの専門知識を評価する際にエンジニアリングチームが実際に質問する内容を反映している。各回答は、面接官が期待する技術的な根拠を要約している。

Q:Vapor Modeはどのような問題を解決し、Vueが既に持っていたVDOM最適化とどう異なるのか?

Vue 3のVDOMコンパイラは、静的サブツリーの最適化、パッチフラグの追加、不要な差分をスキップするためのブロックツリーの実装をすでに行っていた。これらによりVDOMのオーバーヘッドは削減されたが、排除はされなかった。状態変更のたびにVDOMノードの生成、ツリーの走査、パッチの生成が依然として必要だった。Vapor Modeはこのパイプライン全体を除去する。コンパイラがリアクティブ状態を直接DOM操作にマッピングするため、状態変更は必要なDOM操作のみをトリガーする。中間データ構造も、差分アルゴリズムも、破棄されるVDOMノードのガベージコレクションも発生しない。

Q:VaporコンポーネントとVDOMコンポーネントは同一アプリケーション内で共存できるか?

可能である。vaporInteropPluginにより、同一のコンポーネントツリー内で両方のコンポーネントタイプを使用できる。VDOMの親がVaporの子をレンダリングでき、その逆も可能である。ただし注意点がある。VaporスロットはVDOMコンポーネント内でslots.default()を使用できない(代わりにrenderSlotを使用)。RC.3以降の修正により、VDOMの親でラップされたVaporコンポーネントではSuspenseがサポートされるようになった。VDOMベースのコンポーネントライブラリ(Vuetify、PrimeVue)との相互運用は、本番デプロイ前に十分にテストする必要がある。

Q:Vue 3.6のAlien Signalsにおけるプッシュプルリアクティビティモデルを説明せよ。

プッシュプルモデルはリアクティブ更新を2つのフェーズに分割する。プッシュフェーズでは、シグナルの値が変更されると、すべての依存するcomputed propertyに対してdirtyフラグを下流に伝播する。これはブーリアンフラグを反転させるだけなので低コストである。プルフェーズでは、computed値が実際に読み取られる際にdirtyかどうかを確認する。dirtyであれば依存関係から再計算し、クリーンであればキャッシュ値を返す。これにより、特定の更新サイクルで読み取られることのないcomputed propertyを先行計算する問題を回避できる。

Q:Vue 3アプリケーションでrefの代わりにshallowRefを使うべき状況は?

shallowRefは、データ構造が大きく、トップレベルの再割り当てのみでリアクティビティをトリガーすべき場合に適切である。APIレスポンスのキャッシュ、設定オブジェクト、個別のアイテム変更をtriggerRef()で手動制御する大規模配列などが該当する。ディープリアクティビティはすべてのネストされたプロパティをProxyでラップするが、これはデータを個別に変更するのではなく一括で置き換える場合には不要なオーバーヘッドである。

Vue.jsのcomposablesやリアクティビティパターンに関する面接対策問題は、SharpSkillでさらに練習できる。

参考資料

公式のVue.jsパフォーマンスガイドでは、propsの安定性、仮想スクロール、SSRストリーミングなどの追加最適化テクニックが解説されている。Vue 3.6 RCリリースノートには、最新のVapor Mode APIと既知の制限事項が記載されている。

Sources

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

Vue 3.6 Vapor Modeが本番アプリケーションに与える影響

Vapor ModeはVue SFCを直接DOM操作にコンパイルし、仮想DOMの生成・差分処理・パッチ適用のオーバーヘッドを完全に排除する。<script setup vapor>でコンポーネント単位で有効化でき、アプリケーション全体の移行は不要である。

Alien Signalsは新しいデフォルトリアクティビティエンジンであり、メモリ使用量を14%削減し、プッシュプル遅延評価によりcomputed propertyのパフォーマンスを向上させる。

大規模データ構造にはshallowRef、高コストリストレンダリングにはv-memo(VDOMのみ)、コード分割にはdefineAsyncComponentを使用する。これらのパターンはVDOMとVaporの両モードで適用可能である。

Vapor Modeは<script setup>を用いたComposition APIが必須である。Options API、getCurrentInstance()、v-memo、globalPropertiesはサポートされない。

まずVue DevToolsでプロファイリングを行い、影響の大きいコンポーネントをVaporに変換し、効果を測定してから段階的に拡大する。

Vue 3.6は2026年9月時点でリリース候補(RC)段階にある。RC.7ではハイドレーション、KeepAlive、Teleport、VDOMとのSuspense相互運用が対応された。本番デプロイ前に十分なテストを行う必要があるが、API表面は本格的な評価に十分な安定性に達している。

今日のチャレンジ

Vue.js / Nuxt.js のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年9月7日 更新

タグ

#vue
#vue-3
#vapor-mode
#performance
#virtual-dom
#alien-signals

共有

関連記事