Skip to main content

Keepalive in Vue.js

<KeepAlive> is Vue's built-in component that caches deactivated child components in memory instead of destroying them, preserving their state and DOM across toggles.

Theory

TL;DR

  • Think of it like pausing a video game instead of quitting: your progress stays exactly where you left it.
  • v-if destroys and remounts (state is gone); <KeepAlive> deactivates and reactivates (state stays).
  • Use it when toggling views more than twice per session and the state matters (forms, scroll position, lists).
  • Skip it for one-time views or memory-heavy components you don't revisit.
  • Two extra lifecycle hooks come with it: onActivated and onDeactivated.

Quick example

vue
<template> <button @click="tab = 'home'">Home</button> <button @click="tab = 'form'">Form</button> <!-- Without KeepAlive: switch tabs, form input resets --> <!-- <Form v-if="tab === 'form'" /> --> <!-- With KeepAlive: type "hello", switch away, come back — still "hello" --> <KeepAlive> <Home v-if="tab === 'home'" /> <Form v-else /> </KeepAlive> </template> <script setup> import { ref } from 'vue' const tab = ref('home') </script>

Type something in the Form, switch to Home, switch back. The input is still there. That is the whole point.

Key difference from v-if

Without <KeepAlive>, v-if runs the full destroy cycle: beforeUnmount fires, the DOM clears, all reactive state and watchers vanish. On remount, everything starts from scratch. With <KeepAlive>, Vue moves the component to a hidden cache container outside the active DOM tree. The vnode tree, reactive state, and event listeners all stay intact. On reactivation, Vue re-inserts the exact same vnode and calls onActivated.

When to use

  • Tabs with user input → use <KeepAlive> (scroll position and form data persist).
  • Multi-step forms where users go back → use it (no progress lost).
  • List → Detail → Back navigation → use it (list stays scrolled where the user left it).
  • Memory-intensive views the user visits once → skip it (cache grows and stays in memory).
  • Static pages that always load fresh data → skip it (the cache adds overhead with no benefit).

Controlling the cache: include, exclude, and max

By default, <KeepAlive> caches every component it wraps. That can be a problem in large apps. Three props give you control:

vue
<!-- Cache only components named ProductList and ShoppingCart --> <KeepAlive include="ProductList,ShoppingCart"> <component :is="currentView" /> </KeepAlive> <!-- Cache everything except Settings --> <KeepAlive exclude="Settings"> <component :is="currentView" /> </KeepAlive> <!-- LRU: keep at most 5 instances; oldest gets evicted when limit hits --> <KeepAlive :max="5"> <component :is="currentView" /> </KeepAlive>

include and exclude match against the component's name option, not the HTML tag or file name. If a component has no name, the filter ignores it and it falls back to normal v-if behavior. Always set a name on components you plan to cache selectively.

Lifecycle hooks

<KeepAlive> adds two hooks that replace the normal mount/unmount pair for cached components:

vue
<script setup> import { onActivated, onDeactivated } from 'vue' // Fires when the component enters the DOM from cache onActivated(() => { fetchLatestData() // refresh stale data }) // Fires when the component leaves the DOM but stays in cache onDeactivated(() => { clearInterval(timer) // pause background work }) </script>

onDeactivated is not beforeUnmount. The component is still alive; it just left the screen. onActivated is not onMounted either — it fires every time the component comes back, not just the first time. On the very first render, both onMounted and onActivated fire.

How it works internally

Vue 3's renderer keeps a Map of cached vnodes keyed by component name or uid. When a component is deactivated, the renderer moves it to a hidden container outside the live DOM tree. Nothing is destroyed. When the user switches back, Vue pulls the vnode from the Map, re-inserts it, and patches only what changed. LRU eviction with :max tracks activation order and drops the oldest entry when the limit is hit.

With Vue Router

Route-level caching is a common pattern in SPAs. The standard setup looks like this:

vue
<!-- App.vue --> <template> <RouterView v-slot="{ Component }"> <KeepAlive include="Home,Dashboard"> <component :is="Component" :key="$route.path" /> </KeepAlive> </RouterView> </template>

Adding :key="$route.path" means different route paths get separate cache entries even for the same component name. Useful for a user profile page where /users/1 and /users/2 should have independent state.

Common mistakes

1. No name on the component. include="UserForm" matches the name option, not the file name. A component without name will not be cached selectively.

vue
<!-- Wrong: include filter has nothing to match --> <KeepAlive include="UserForm"> <component :is="currentView" /> <!-- UserForm.vue has no name option --> </KeepAlive> <!-- Fix: set name via defineOptions --> <script setup> defineOptions({ name: 'UserForm' }) </script>

2. No :max in a dynamic tab app. Without a limit, every cached component stays in memory indefinitely. In production dashboard apps, this can steadily consume hundreds of megabytes before anyone notices. On mobile, 20+ cached views cause noticeable slowdowns.

vue
<!-- Wrong: unbounded cache --> <KeepAlive> <component :is="currentView" /> </KeepAlive> <!-- Fix: set a reasonable limit --> <KeepAlive :max="10"> <component :is="currentView" /> </KeepAlive>

3. Using :key without knowing it evicts the cache. Adding :key to the cached component tells Vue to treat each unique key as a separate instance. Changing the key forces a new cache entry, which means the old state is gone even with <KeepAlive>. Use :key intentionally for route-specific caching, not accidentally.

4. Treating onDeactivated like beforeUnmount. Cleanup in onDeactivated should only pause things (timers, subscriptions). If you manually clear reactive state there, the component breaks on reactivation.

5. include with HTML tag names does nothing. <KeepAlive include="div"> caches nothing. It matches component names only.

Real-world usage

  • Vue Router in admin dashboards: cache Home and Dashboard so filters and table state survive navigation.
  • Element Plus and Vant UI: their tab panel components wrap content in <KeepAlive> to keep form state between tabs.
  • Nuxt 3: page-level caching via keepalive route meta for e-commerce product listing pages.
  • Pinia devtools: caches store inspector panels to retain filter state between inspections.

Follow-up questions

Q: What is the difference between onDeactivated and beforeUnmount?
A: onDeactivated fires when the component leaves the DOM but stays alive in the cache. beforeUnmount fires when the component is about to be fully destroyed. With <KeepAlive>, beforeUnmount does not fire on toggle — only when the entry is evicted from the cache or the parent is destroyed.

Q: How does LRU eviction work with :max?
A: Vue tracks the activation order in a list. When a new component would push the count past :max, the least recently activated entry is evicted and fully unmounted. Standard LRU behavior.

Q: Why does onActivated fire on the first mount too?
A: Vue calls both onMounted and onActivated on the first render. If you need first-mount-only logic, put it in onMounted. onActivated is for logic that should run every time the component enters the DOM.

Q: How do you cache a component differently per route param, like /users/1 vs /users/2?
A: Use :key="$route.fullPath" or :key="$route.params.id" on the cached component. Each unique key creates a separate cache entry, so both user profiles get their own state.

Q: Does <KeepAlive> affect server-side rendering?
A: No. <KeepAlive> is client-only. It does nothing during SSR and activates after hydration on the client.

Examples

Basic tab navigation with cache control

vue
<template> <div> <button @click="activeTab = 'products'">Products</button> <button @click="activeTab = 'cart'">Cart</button> <KeepAlive include="ProductList,ShoppingCart" :max="5"> <ProductList v-if="activeTab === 'products'" /> <ShoppingCart v-else /> </KeepAlive> </div> </template> <script setup> import { ref } from 'vue' import ProductList from './ProductList.vue' import ShoppingCart from './ShoppingCart.vue' // Both components need defineOptions({ name: 'ProductList' }) etc. const activeTab = ref('products') </script>

Add items to the cart, switch to Products, switch back. The cart quantities are still there. This is the pattern used in most e-commerce SPAs with tab-based navigation.

Refreshing data on every reactivation

vue
<script setup> import { ref, onActivated } from 'vue' const orders = ref([]) async function loadOrders() { orders.value = await fetch('/api/orders').then(r => r.json()) } // Runs on first mount AND every time this tab is revisited onActivated(() => { loadOrders() }) </script>

The component keeps its DOM and scroll position from the cache but loads fresh data on each activation. Users see the list instantly (from cache) and then see updated content once the fetch completes.

Route-level caching with per-route keys

vue
<!-- App.vue --> <template> <RouterView v-slot="{ Component }"> <KeepAlive :max="10"> <component :is="Component" :key="$route.fullPath" /> </KeepAlive> </RouterView> </template>

:key="$route.fullPath" gives /dashboard/sales and /dashboard/analytics separate cache slots even if they use the same component. Without the key, both routes would share one cached instance and one state — which is almost never what you want.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?