Vue 3 KeepAliveガイド:ルートキャッシュ、状態保持、メモリ管理
<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
}
}
]
include や exclude を使う場合、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の価値は状態保持にあります。自動的に速くなる魔法ではありません。
ブラウザローカルツールを無料で試す →