Vue 3 KeepAlive Guide: Route Cache, State Preservation, and Memory Control
<KeepAlive> is Vue's built-in component for caching inactive component instances. It is useful when a user leaves a page and later returns to it, expecting filters, form state, scroll position, and local UI state to remain intact.
KeepAlive is also easy to misuse. If every route is cached, memory grows. If stale data is never refreshed, users see old information. If component names and route keys are not controlled, caching may behave differently than expected.
Useful references:
When KeepAlive Is Worth Using
Use KeepAlive for pages where preserving local UI state improves the workflow:
- A list page with filters, search text, selected tabs, and scroll position.
- A multi-step form where the user may inspect another page and return.
- A dashboard with expensive local rendering state.
- A tabbed admin page where switching tabs should not reset each tab.
Avoid KeepAlive when:
- The page is cheap to recreate.
- The page must always show fresh data.
- The page contains sensitive data that should disappear when the user leaves.
- The component owns long-running timers, subscriptions, or large in-memory objects that are hard to pause.
The default should be selective caching, not caching every route.
Basic Route-Level Setup
Vue Router exposes the rendered route component through the RouterView slot. That is the cleanest place to wrap selected pages with 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>
Route metadata keeps the decision near the route definition.
// 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
}
}
]
Keep component names stable when using include or exclude, because Vue matches cached components by name.
<!-- UserList.vue -->
<script setup lang="ts">
defineOptions({
name: "UserList"
})
</script>
Refresh Data Without Resetting UI State
The most common bug is stale data. A cached page can keep old API results even after the user returns from another route.
Use onActivated to check whether data should refresh. Do not blindly reload everything on every activation, or you lose much of the benefit of caching.
<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(() => {
// Pause work that should not continue while the page is hidden.
})
</script>
This preserves local UI state while still letting data update when it is old.
Preserve Scroll Position
KeepAlive preserves the component instance, but scroll behavior can still be affected by layout, route transitions, or container scroll.
For a scrollable container, store and restore its position:
<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>
If you rely on browser-level scroll restoration, test the behavior across direct navigation, back button navigation, and programmatic route changes.
Control Cache Size
The max prop limits how many component instances KeepAlive retains. When the limit is exceeded, Vue prunes older cached instances.
<KeepAlive :max="8">
<component :is="Component" />
</KeepAlive>
Pick a limit based on real user workflows. A small admin app may need only a few cached list pages. A tab-heavy internal system may need more. Avoid setting a high number just to hide state-management problems.
Avoid Memory Leaks
Cached components are deactivated, not unmounted. That means some cleanup belongs in onDeactivated, while final cleanup still belongs in onUnmounted.
Pause or remove:
- Intervals and timeouts.
- WebSocket or SSE listeners that should not run in the background.
- ResizeObserver and IntersectionObserver instances.
- Global event listeners.
- Large temporary arrays, maps, or blobs that can be recreated.
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)
If a page still consumes memory after repeated navigation, profile it in browser devtools instead of guessing.
Cache Keys And Dynamic Routes
Dynamic routes need careful keys. If all detail pages use the same component name and the same key, they may share a cached instance in a way you did not intend.
Use one of these patterns:
<!-- Share one cached instance for the user list -->
<component :is="Component" :key="route.name" />
<!-- Separate cache entry for each route parameter -->
<component :is="Component" :key="route.fullPath" />
<!-- Controlled custom key -->
<component :is="Component" :key="route.meta.cacheKey || route.fullPath" />
Do not use fullPath casually for list pages with many query combinations. It can create too many cache entries. For list pages, it is often better to cache one instance and store filters inside the component or a store.
Practical Rules
Use KeepAlive when the page has valuable local state. Do not use it as a generic performance switch.
Good candidates:
- Search result lists.
- Form drafts.
- Dashboards with expensive chart setup.
- Tabs where users expect instant switching.
Poor candidates:
- Authentication screens.
- Payment or security-sensitive pages.
- Pages that must refetch every time.
- Detail pages with many unique IDs unless there is a clear cache limit.
Final Checklist
Before shipping route-level caching, confirm:
- Only selected routes use KeepAlive.
- Cached components have stable names.
- Dynamic routes have intentional keys.
maxis set to a reasonable value.- Data refresh uses
onActivatedwhen needed. - Timers, observers, and listeners pause on deactivation.
- Sensitive pages are not cached.
- Back button, direct navigation, and tab switching are tested.
KeepAlive is reliable when treated as state preservation, not as automatic performance magic.
Try these browser-local tools — no sign-up required →