Vue 3 の動的コンポーネントと非同期コンポーネント:コンポーネント + Suspense
動的コンポーネントを使用すると、単一の <component> タグを使って異なるコンポーネントを動的にレンダリングでき、データに基づいてタブ、モーダル、ルートビューを切り替えることができます。非同期コンポーネントを使用すると、必要に応じてコンポーネントを読み込むことができ、最初の画面では必要なコードのみを読み込むことでパフォーマンスを向上させることができます。
Vue 3の<Suspense>実験的コンポーネントは、非同期コンポーネントの読み込み状態を処理するために特別に設計されており、v-if="loading"を手動で記述する必要がなくなります。このレッスンでは、3つの主要なAPIを習得できます。
1. 学習内容
<component :is="...">ダイナミックコンポーネントの基礎:isの記述方法:文字列 / コンポーネントオブジェクトdefineAsyncComponent非同期読み込み- コード分割(動的インポート)によるパフォーマンス上の利点
<Suspense>非同期コンポーネントの取り扱い- 動的コンポーネント + キープアライブキャッシュ
- 5つの実例と3つのアンチパターン
2. 1つのタブに5つのv-if文が並ぶ悪夢
(1) 課題:5つのタブ、10個のネストされた v-if ディレクティブ
アリスはタブ付きの管理用ダッシュボードを作成しました:
<!-- ❌ The "Flip" Version: 5 Tabs with 5 v-if -->
<template>
<div class="tabs">
<button @click="currentTab = 'home'">Home</button>
<button @click="currentTab = 'profile'">Profile</button>
<button @click="currentTab = 'settings'">Settings</button>
<div v-if="currentTab === 'home'">
<HomeTab />
</div>
<div v-else-if="currentTab === 'profile'">
<ProfileTab />
</div>
<div v-else-if="currentTab === 'settings'">
<SettingsTab />
</div>
</div>
</template>
プロダクトマネージャーのチャーリー:
「アリス、タブをあと5つ増やして。v-if-elseが10行もあると保守性が悪くなる。動的コンポーネントを使って。」
(2) Vueの動的コンポーネントによる解決策:1つの<component>で5つのタブを切り替える
<!-- ✅ Correct Version: 1 <component> Switch -->
<template>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.name"
:class="{ active: currentTab === tab.name }"
@click="currentTab = tab.name"
>
{{ tab.label }}
</button>
<!-- ✅ 1 <component> Dynamic Tag Switching -->
<component :is="currentTabComponent" />
</div>
</template>
<script setup>
import { ref, computed, defineAsyncComponent } from 'vue'
// Synchronization Components
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'
// Asynchronous Components(Laden on Demand)
const SettingsTab = defineAsyncComponent(() => import('./tabs/SettingsTab.vue'))
const tabs = [
{ name: 'home', label: 'Home' },
{ name: 'profile', label: 'Profile' },
{ name: 'settings', label: 'Settings' }
]
const currentTab = ref('home')
// Computed Properties:According to currentTab Determine which component to display
const currentTabComponent = computed(() => {
return { home: HomeTab, profile: ProfileTab, settings: SettingsTab }[currentTab.value]
})
</script>
(3) 収益
動的コンポーネントを使用した後:
- コードサイズ:v-if 10行 →
<component>1行(-90%) - 拡張性:新しいタブを追加するには、インポートを1回とコンポーネントオブジェクトを1つ用意するだけで済みます
- 初回画面表示時のパフォーマンス:[設定]タブの非同期読み込みにより、約50KBの容量を節約
- 保守性:1つの配列で5つのタブを管理
3. <component :is="..."> 基礎
(1) :is の5つの活用法
<!-- Writing Style 1:Component Name String(Registered globally) -->
<component :is="'HomeTab'" />
<!-- Writing Style 2:Component Object(Most Commonly Used) -->
<component :is="HomeTab" />
<!-- Writing Style 3:Dynamically Calculated Properties -->
<component :is="currentTabComponent" />
<!-- Writing Style 4:Asynchronous Component Object -->
<component :is="asyncComponent" />
<!-- Writing Style 5:Inline Component Object -->
<component :is="{ template: '<div>Inline</div>' }" />
(2) 完全な例
<!-- TabContainer.vue -->
<template>
<div>
<button
v-for="tab in tabs"
:key="tab.name"
@click="currentTab = tab.name"
>
{{ tab.label }}
</button>
<!-- Dynamic Components -->
<component :is="currentTabComponent" />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'
import SettingsTab from './tabs/SettingsTab.vue'
const tabs = [
{ name: 'home', label: 'Home' },
{ name: 'profile', label: 'Profile' },
{ name: 'settings', label: 'Settings' }
]
const currentTab = ref('home')
const currentTabComponent = computed(() => ({
home: HomeTab,
profile: ProfileTab,
settings: SettingsTab
}[currentTab.value]))
</script>
(3) 5つの主な活用シナリオ
| シーン | ダイナミックコンポーネント |
|---|---|
| タブの切り替え | ✅ |
| 脚本・台詞 | ✅ |
| 複数ステップのフォーム | ✅ |
| ルートビュー | ✅ (内部コンポーネント <router-view>) |
| テーマの切り替え(異なるレイアウト) | ✅ |
4. 非同期コンポーネント defineAsyncComponent
(1) なぜ非同期コンポーネントが必要なのでしょうか?
標準コンポーネントは、import時にすべてのコードを即座に読み込みます。リッチテキストエディタやチャートライブラリなどの大規模なコンポーネントは、最初の画面表示には必ずしも必要ではありませんが、読み込みを妨げてしまいます。非同期コンポーネントは必要に応じて読み込まれるため、最初の画面表示が高速化されます。
<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
// Asynchronous Components:Only when using ChartEditor Load its code only when needed
const ChartEditor = defineAsyncComponent({
// 1. Load Function
loader: () => import('./ChartEditor.vue'),
// 2. Display while loading
loadingComponent: LoadingSpinner,
// 3. Display when loading fails
errorComponent: ErrorMessage,
// 4. Load Delay(Prevent Flickering)
delay: 200
})
</script>
<template>
<ChartEditor v-if="showEditor" />
</template>
(2) 簡略化された表記法
<script setup>
import { defineAsyncComponent } from 'vue'
// ✅ Simplified Version:Only loader
const ChartEditor = defineAsyncComponent(() => import('./ChartEditor.vue'))
</script>
(3) 非同期コンポーネントに関する5つの主要なシナリオ
| シナリオ | 非同期読み込み |
|---|---|
| リッチテキストエディタ | ✅ |
| チャートライブラリ (ECharts/D3) | ✅ |
| 大規模なフォーム(フィールド数が100以上) | ✅ |
| モーダルポップアップ | ✅ |
| ルートの遅延読み込み | ✅ (Vue Router によって自動的に使用されます) |
5. <Suspense> 非同期コンポーネントの取り扱い
(1) 「サスペンス」とは何か?
Suspenseコンポーネントは、Vue 3の非同期コンポーネント読み込みコーディネーターであり、非同期コンポーネントの読み込み状態やエラー状態を自動的に処理します。手動でv-if="loading"を書く手間とはもうおさらばです。
<template>
<!-- Suspense Automatically wait for asynchronous components to finish loading -->
<Suspense>
<!-- Asynchronous Components -->
<ChartEditor :data="chartData" />
<!-- Display while loading(Default Slot) -->
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
(2) 2スロット
<template>
<Suspense>
<!-- Default Slot:Asynchronous Content -->
<AsyncComponent />
<!-- Fallback Slot: Display while loading -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
(3) 5つの主な利点
| 利点 | 説明 |
|---|---|
| シンプルさ | 読み込み状態を手動でコーディングする必要がない |
| 統合 | 複数の非同期コンポーネントの統合処理 |
| ネスト | ネストされたSuspenseに対応 |
| エラー処理 | onErrorCaptured と連携 |
| SSR対応 | サーバーサイドの自動待機 |
6. 動的コンポーネントとキープアライブキャッシュ
(1) デフォルトの動作:コンポーネントを切り替えると、それらは破棄され、再構築される
<template>
<component :is="currentTabComponent" />
</template>
<!-- Switch Tab:
1. Legacy Components onBeforeUnmount
2. Legacy Components onUnmounted
3. New Component onBeforeMount
4. New Component onMounted
5. Data is reloaded every time you switch(Poor performance)
-->
(2) キープアライブキャッシュ(重複作成を防ぐため)
<template>
<keep-alive>
<component :is="currentTabComponent" />
</keep-alive>
</template>
<!-- Switch Tab:
1. Legacy Components onDeactivated(Destroy without stopping)
2. New Component onActivated(Restore from Cache)
3. Data Retention,Scroll Position Retention
-->
(3) keep-alive の 3 つの主要な設定
<template>
<!-- include:Cache only the specified components -->
<keep-alive include="HomeTab,ProfileTab">
<component :is="currentTabComponent" />
</keep-alive>
<!-- exclude:Do not cache the specified component -->
<keep-alive exclude="SettingsTab">
<component :is="currentTabComponent" />
</keep-alive>
<!-- max: Maximum 5 Cached -->
<keep-alive :max="5">
<component :is="currentTabComponent" />
</keep-alive>
</template>
7. 完全な例:動的なタブと非同期読み込み
(1) ▶ サンプル:. ダイナミックタブの基本
<template>
<div>
<nav>
<button
v-for="tab in tabs"
:key="tab.name"
:class="{ active: currentTab === tab.name }"
@click="currentTab = tab.name"
>
{{ tab.label }}
</button>
</nav>
<component :is="currentTabComponent" />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'
const tabs = [
{ name: 'home', label: 'Home' },
{ name: 'profile', label: 'Profile' },
{ name: 'settings', label: 'Settings' }
]
const currentTab = ref('home')
const currentTabComponent = computed(() => ({
home: HomeTab,
profile: ProfileTab,
settings: SettingsTab
}[currentTab.value]))
</script>
(2) ▶ サンプル:. 非同期コンポーネント + Suspense
<!-- App.vue -->
<template>
<button @click="show = !show">Toggle</button>
<Suspense v-if="show">
<!-- Asynchronous Components(Do not load on the first screen,Load only when clicked) -->
<HeavyChart :data="data" />
<template #fallback>
<div>Loading chart...</div>
</template>
</Suspense>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const show = ref(false)
// ✅ Asynchronous Components:News import,Laden on Demand
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
const data = ref([1, 2, 3, 4, 5])
</script>
(3) ▶ サンプル:. 動的コンポーネントとキープアライブキャッシュ
<template>
<div>
<button v-for="tab in tabs" @click="currentTab = tab.name">
{{ tab.label }}
</button>
<!-- keep-alive Cache: Preserve data when switching -->
<keep-alive :max="3">
<component :is="currentTabComponent" />
</keep-alive>
</div>
</template>
(4) ▶ サンプル:. :is の 5 つの使い方の比較
| 構文 | 例 | 使い方 |
|---|---|---|
| 文字列 | <component :is="'HomeTab'"> |
グローバル登録(推奨されません) |
| コンポーネントオブジェクト | <component :is="HomeTab"> |
静的インポート(最も一般的) |
| 非同期コンポーネントオブジェクト | <component :is="AsyncComponent"> |
オンデマンド読み込み |
| 計算プロパティ | <component :is="currentTabComponent"> |
動的切り替え |
| インラインオブジェクト | <component :is="{ template: '...' }"> |
簡単なシナリオ |
(5) ▶ サンプル:. よくある5つの間違いに関するクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
| :は文字列ですが、グローバルに登録されていません | 警告: 未知のコンポーネント | コンポーネントオブジェクトを使用するか、まず登録してください |
| 非同期コンポーネントにはフォールバックがない | 読み込み中に画面が真っ白になる | Suspenseでラップする |
| コンポーネントを切り替えてデータを再読み込み | パフォーマンスの低下 | キープアライブキャッシュを使用 |
| 非同期コンポーネントにおける未処理のエラー | サイレントな失敗 | onErrorCaptured との併用 |
| 動的コンポーネントに :key を追加し忘れた | 状態の不整合 | :key="tab.name" を追加 |
(6) ▶ サンプル:. 5つの主要なパフォーマンス比較
| モード | 最初の画面 | 切り替え | 適用対象 |
|---|---|---|---|
| 直接インポート | 低速 | 再読み込み | ウィジェット |
| 動的インポート | 高速 | 最初の切り替え時に遅い | 大規模なコンポーネント |
| サスペンス | 高速 | スムーズ | ユーザー体験を最優先 |
| キープアライブ | 高速 | 0 リロード | 頻繁な切り替え |
| 非同期 + キープアライブ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ベストプラクティス |
❓ よくある質問
<component>タグを使用し、:is属性に基づいてコンポーネントを切り替えるため、より簡潔になります。また、非同期コンポーネントはオンデマンドで読み込むことも可能です。defineAsyncComponent は setup の最上位レベルにある必要がありますか?defineAsyncComponent(() => import('...')) は <script setup> の最上位レベル(関数内ではなく)で呼び出さなければなりません。v-bind を直接使用します:<component :is="Comp" :prop1="x" :prop2="y" />。errorComponent オプションと onErrorCaptured: { loader, errorComponent: ErrorComp } を使用します。Suspense 内では、コードを onErrorCaptured で囲むことができます。keep-alive によってキャッシュされたコンポーネントデータは永続化されますか?defineAsyncComponentとVue Routerの遅延読み込みの関係はどのようなものですか?defineAsyncComponentを使用して、ルートの遅延読み込みを実装しています。本質的には、これらは同じものです。📖 まとめ
- ダイナミックコンポーネント:
<component :is="...">1つのタブで複数のコンポーネントを切り替える :isの5つの活用法:文字列 / コンポーネントオブジェクト / 非同期 / 計算プロパティ / インライン- 非同期コンポーネント
defineAsyncComponent:オンデマンドで読み込みを行い、最初の画面のレンダリングを高速化 - サスペンス:非同期コンポーネントの読み込み状態を自動的に処理します
- キープアライブ + 動的コンポーネント:重複生成を防ぐためのキャッシュ
- 5つの主要なシナリオ:タブ / モーダル / ルーティング / 複数ステップのフォーム / テーマ
- パフォーマンスのベストプラクティス:非同期処理 + キープアライブ + サスペンス
📝 練習問題
(1) 基礎問題(難易度:⭐)
簡単なタブ切り替え機能を実装する:
- 3つのタブ(ホーム/プロフィール/設定)
<component :is>を使用して動的に切り替える- Switchでの出力:「コンポーネント X が作成/破棄されました」
(2) 上級問題(難易度:⭐⭐)
タブ機能と非同期読み込みの実装:
- 3つのタブ。「設定」タブは
defineAsyncComponentを使用して非同期で読み込まれます - 読み込み状況を表示する(読み込み中は「v」の文字を手書きで表示)
- 「ホーム」タブに戻っても再読み込みされない
(3) チャレンジ問題(難易度:⭐⭐⭐)
完全な動的・非同期・キャッシュ機能を備えたシステムの実装:
- タブが5つあり、そのうち少なくとも2つは非同期で読み込まれる
<Suspense>で包む<keep-alive :max="3">キャッシュ- 切り替え時に onActivated/onDeactivated を出力する
- 非同期コンポーネントが失敗した際に、ErrorComponent を表示する
- ファーストスクリーンの読み込み時間を測定する(非同期化の前後)
- TypeScript:強型付けの動的コンポーネント



