Vue 3 KeepAliveガイド:ルートキャッシュ、状態保持、メモリ管理

前端工程(更新: 2026年7月15日)

<KeepAlive> は、非アクティブになったコンポーネントインスタンスをキャッシュするVue組み込み機能です。ユーザーがページを離れて戻ったとき、フィルター、フォーム下書き、スクロール位置、ローカルUI状態を残したい場合に役立ちます。

一方で、KeepAliveは誤用されやすい機能でもあります。すべてのルートをキャッシュするとメモリが増えます。古いデータを更新しなければ、ユーザーは古い情報を見ます。コンポーネント名やルートkeyが曖昧だと、期待と違うキャッシュ動作になります。

参考ドキュメント:

KeepAliveが向いている場面

KeepAliveは、ローカル状態を保つことで使いやすくなるページに向いています。

  • フィルター、検索語、タブ、スクロール位置を持つ一覧ページ。
  • 途中で別ページを確認して戻る可能性がある複数ステップのフォーム。
  • ローカル描画状態が重いダッシュボード。
  • 切り替えてもリセットしたくない管理画面のタブ。

次のようなページでは避けます。

  • 再作成が安いページ。
  • 毎回必ず最新データを表示すべきページ。
  • 離れたら機密情報を消すべきページ。
  • タイマー、購読、大きなオブジェクトを持ち、停止しにくいコンポーネント。

基本方針は、すべてをキャッシュすることではなく、必要なページだけをキャッシュすることです。

ルート単位の基本構成

Vue Routerは、RouterView のslotで現在のルートコンポーネントを渡します。ここで選択的に KeepAlive を使うのが分かりやすい構成です。

<!-- App.vue -->
<template>
  <RouterView v-slot="{ Component, route }">
    <KeepAlive :include="cachedComponentNames" :max="8">
      <component
        v-if="route.meta.keepAlive"
        :is="Component"
        :key="route.meta.cacheKey || route.name"
      />
    </KeepAlive>

    <component
      v-if="!route.meta.keepAlive"
      :is="Component"
      :key="route.fullPath"
    />
  </RouterView>
</template>

<script setup lang="ts">
const cachedComponentNames = ["UserList", "OrderList", "ReportDashboard"]
</script>

キャッシュするかどうかは、ルートのmetaに書くと管理しやすくなります。

// router.ts
import type { RouteRecordRaw } from "vue-router"

export const routes: RouteRecordRaw[] = [
  {
    path: "/users",
    name: "Users",
    component: () => import("./views/UserList.vue"),
    meta: {
      keepAlive: true,
      cacheKey: "users-list"
    }
  },
  {
    path: "/users/:id",
    name: "UserDetail",
    component: () => import("./views/UserDetail.vue"),
    meta: {
      keepAlive: false
    }
  }
]

includeexclude を使う場合、Vueはコンポーネント名で照合するため、名前を安定させます。

<!-- UserList.vue -->
<script setup lang="ts">
defineOptions({
  name: "UserList"
})
</script>

UI状態を残しながらデータを更新する

よくある問題は、キャッシュされたデータが古くなることです。ユーザーが戻ったとき、一覧データがすでに古くなっている場合があります。

onActivated で更新が必要か判断します。毎回すべて再読み込みすると、キャッシュの価値が下がります。

<script setup lang="ts">
import { onActivated, onDeactivated, ref } from "vue"

const users = ref<User[]>([])
const loading = ref(false)
const lastLoadedAt = ref(0)
const STALE_AFTER_MS = 60_000

async function loadUsers() {
  loading.value = true
  try {
    users.value = await fetchUsers()
    lastLoadedAt.value = Date.now()
  } finally {
    loading.value = false
  }
}

onActivated(() => {
  const isStale = Date.now() - lastLoadedAt.value > STALE_AFTER_MS
  if (!users.value.length || isStale) {
    void loadUsers()
  }
})

onDeactivated(() => {
  // 非表示中に続けるべきでない処理を止める。
})
</script>

これにより、フィルターやスクロールなどの状態を残しながら、古いデータだけを更新できます。

スクロール位置を保持する

KeepAliveはコンポーネントインスタンスを残しますが、レイアウト、トランジション、スクロールコンテナによってスクロール位置が変わることがあります。

内部スクロールコンテナを使う場合は、位置を保存して復元します。

<template>
  <main ref="scrollEl" class="page-scroll">
    <!-- list content -->
  </main>
</template>

<script setup lang="ts">
import { nextTick, onActivated, onDeactivated, ref } from "vue"

const scrollEl = ref<HTMLElement | null>(null)
let savedTop = 0

onDeactivated(() => {
  savedTop = scrollEl.value?.scrollTop ?? 0
})

onActivated(async () => {
  await nextTick()
  if (scrollEl.value) {
    scrollEl.value.scrollTop = savedTop
  }
})
</script>

ブラウザのスクロール復元に頼る場合も、直接アクセス、戻るボタン、プログラムによる遷移をすべて確認してください。

キャッシュ数を制限する

max は、KeepAliveが保持するコンポーネントインスタンス数を制限します。上限を超えると、Vueは古いキャッシュを破棄します。

<KeepAlive :max="8">
  <component :is="Component" />
</KeepAlive>

上限は実際の操作に合わせます。小さな管理画面なら数ページで十分なことがあります。タブが多い内部システムではもう少し必要かもしれません。状態管理の問題を隠すために大きな値を設定しないでください。

メモリリークを避ける

キャッシュされたコンポーネントはアンマウントされるのではなく、非アクティブになります。そのため、一部の停止処理は onDeactivated に置き、最終的な後始末は onUnmounted に置きます。

停止または削除すべきもの:

  • intervalやtimeout。
  • 非表示中に動くべきでないWebSocketやSSEリスナー。
  • ResizeObserverやIntersectionObserver。
  • グローバルイベントリスナー。
  • 再作成できる大きな配列、Map、Blob。
import { onActivated, onDeactivated, onUnmounted } from "vue"

let timer: number | undefined

function startPolling() {
  timer = window.setInterval(refreshStatus, 15_000)
}

function stopPolling() {
  if (timer) {
    window.clearInterval(timer)
    timer = undefined
  }
}

onActivated(startPolling)
onDeactivated(stopPolling)
onUnmounted(stopPolling)

ページを何度も切り替えた後もメモリが増え続ける場合は、推測ではなくブラウザ開発者ツールでプロファイルします。

キャッシュKeyと動的ルート

動的ルートではkeyを慎重に決めます。複数の詳細ページが同じコンポーネント名と同じkeyを使うと、意図せず同じキャッシュインスタンスを共有することがあります。

よく使うパターン:

<!-- ユーザー一覧で1つのキャッシュを共有 -->
<component :is="Component" :key="route.name" />

<!-- ルートパラメータごとに別キャッシュ -->
<component :is="Component" :key="route.fullPath" />

<!-- 明示的なカスタムkey -->
<component :is="Component" :key="route.meta.cacheKey || route.fullPath" />

一覧ページで安易に fullPath を使うのは避けます。クエリの組み合わせが多いと、キャッシュが増えすぎるためです。一覧では、1つのインスタンスをキャッシュし、フィルターをコンポーネントまたはstoreに持たせるほうが扱いやすいことが多いです。

実用ルール

KeepAliveは価値のあるローカル状態を保持するために使います。汎用のパフォーマンススイッチとして使うものではありません。

向いているもの:

  • 検索結果一覧。
  • フォーム下書き。
  • グラフ初期化が重いダッシュボード。
  • 素早く切り替えたいタブ。

向いていないもの:

  • ログインや認証ページ。
  • 決済やセキュリティ関連ページ。
  • 毎回再取得すべきページ。
  • 多数の異なるIDを持つ詳細ページ。ただし明確な上限がある場合は別です。

公開前チェック

ルートキャッシュを出す前に確認します。

  • 選んだルートだけがKeepAliveを使う。
  • キャッシュ対象コンポーネントの名前が安定している。
  • 動的ルートkeyが意図的に設計されている。
  • max が適切に設定されている。
  • 必要に応じて onActivated でデータ更新する。
  • タイマー、オブザーバー、リスナーが非アクティブ時に止まる。
  • 機密ページをキャッシュしていない。
  • 戻るボタン、直接アクセス、タブ切り替えをテストしている。

KeepAliveの価値は状態保持にあります。自動的に速くなる魔法ではありません。

ブラウザローカルツールを無料で試す →

#Vue3 KeepAlive#组件缓存#路由缓存#性能优化#include exclude#responsive component caching#layout template cache#2026#前端工程