Vue KeepAlive Cache Memory Growth
Navigating back to a view is instant thanks to <KeepAlive>, but memory grows with every distinct record the user opens, and deactivated components keep polling, animating and listening in the background. This guide from Vue Reactivity and Memory Management, part of Framework-Specific Memory Optimization, explains what a KeepAlive cache holds, how to bound it, and how to make cached components cheap while they are inactive.
| Symptom | Root Cause | Immediate Action | Measurable Impact |
|---|---|---|---|
| Memory grows with each distinct route/record visited | <KeepAlive> without max caches every instance |
Set max (LRU eviction of the least recently used instance) |
Cache size and memory capped |
| Detail pages for every visited ID are cached | Route key includes the ID, so each record is a new instance | Cache list views, not per-ID detail views; use include/exclude |
Only valuable views cached |
| Hidden views still poll the API or animate | Work started in onMounted continues when deactivated |
Pause in onDeactivated, resume in onActivated |
No background CPU, allocation or network |
| Cached instances hold large datasets | State and DOM retained for instant restore | Drop heavy data in onDeactivated, refetch on activation |
Smaller cached instances |
| Old user’s views visible to the next user | Cache survives logout | Reset the KeepAlive (key change) on logout | Memory and privacy reset |
Root Cause: KeepAlive Deliberately Retains Whole Component Instances
When a component inside <KeepAlive> is switched away from, Vue does not unmount it. It deactivates it: the component’s DOM subtree is moved into a detached container, its reactive state, computed values, watchers and child components stay in memory, and it is stored in the KeepAlive’s cache keyed by the component’s key (or its type). Switching back re-inserts the same DOM and state instantly. That is the whole point — and it means each cached entry costs as much memory as the mounted view: DOM nodes, component instances, reactive proxies, and whatever data the view loaded.
Without a max, the cache keeps every instance it has ever seen. How many that is depends on the cache key. If <router-view> renders with a key derived from the full path (a common pattern to force re-creation per record), then /orders/1, /orders/2, … /orders/500 are 500 separate cached instances, each holding its record, its DOM and its watchers. With max, KeepAlive evicts the least recently used instance — genuinely unmounting it — when the limit is exceeded, turning unbounded growth into a fixed ceiling.
The second cost is work that continues while deactivated. Timers, intervals, polling, animation frames, WebSocket subscriptions and window listeners started in onMounted keep running because the component was never unmounted. Each keeps allocating and holds closures over the component’s state. Vue provides onActivated and onDeactivated lifecycle hooks for exactly this: pause work on deactivation and resume on activation. Cleanup in onUnmounted still matters for instances eventually evicted or for normal unmounts. The broader effect-scope rules that govern watchers and computed values are covered in preventing memory leaks in Vue watchers and computed.
Step-by-Step Fix
- Measure growth per visited view. Visit twenty different records, take a heap snapshot, and look at retained size of component instances and detached DOM (KeepAlive keeps inactive DOM detached). Verification: retained memory grows roughly linearly with the number of distinct records visited.
- Set
max. Add amaxprop to<KeepAlive>sized to how many views users realistically switch between (often 3–10). Verification: after visiting more records thanmax, the number of cached instances stays atmax(countonUnmountedcalls or inspect with Vue DevTools). - Restrict what is cached. Use
include/exclude(by component name) so that list views and dashboards are cached but per-record detail views are not, or cache details by type rather than by full path. Verification: detail views unmount on navigation away. - Pause background work. Move interval, polling, animation and subscription start/stop into
onActivated/onDeactivated, keepingonUnmountedcleanup for final teardown. Verification: with a view deactivated, the Network panel shows no polling and the Performance panel no timers from it. - Slim cached state. In
onDeactivated, drop large, cheap-to-refetch data (big lists, charts’ datasets) and restore on activation. Verification: cached instance retained size shrinks in snapshots. - Reset on logout. Change the KeepAlive’s
key(or wrap it in av-ifkeyed on the user) so logout discards all cached views. Verification: after logout, no instances from the previous session remain.
Command and Code Reference
Use case: a bounded, selective KeepAlive around the router view.
<!-- App.vue -->
<template>
<RouterView v-slot="{ Component }">
<!-- Cache only the list/dashboard views, at most 5 instances, LRU-evicted -->
<KeepAlive :max="5" :include="['OrderList', 'Dashboard']" :key="sessionKey">
<component :is="Component" />
</KeepAlive>
</RouterView>
</template>
<script setup>
import { computed } from 'vue';
import { useSession } from './session';
const session = useSession();
const sessionKey = computed(() => session.userId); // new user → fresh cache
</script>
Use case: pause and resume work in a cached component.
<script setup>
import { onActivated, onDeactivated, onUnmounted, ref, shallowRef } from 'vue';
defineOptions({ name: 'Dashboard' }); // name used by :include
const stats = shallowRef(null); // large data: shallow to avoid deep proxies
let timer = null;
function start() {
load();
timer = setInterval(load, 10_000); // polling only while visible
}
function stop() {
clearInterval(timer);
timer = null;
}
async function load() {
stats.value = await fetchStats();
}
onActivated(start); // also runs on first mount inside KeepAlive
onDeactivated(() => {
stop();
stats.value = null; // drop heavy data while cached
});
onUnmounted(stop); // final teardown if evicted or removed
</script>
Verification and Regression Prevention
Verify three properties: after visiting many records, the number of cached instances equals at most max; with a cached view inactive, no timers, polling or listeners from it are active; and after logout, snapshots contain no component instances from the previous session. Vue DevTools shows cached (inactive) components in the component tree, which is a quick way to confirm the cache size during manual testing.
Guard against regressions with an end-to-end test that navigates through many records and asserts that heap usage (via performance.memory in Chromium tests, or CDP metrics) stays under a budget, and lint or review for <KeepAlive> without max. For effects created outside component lifecycles — composables used in stores or plugins — use explicit scopes as described in using effectScope for Vue cleanup.
Edge Cases and Gotchas
Component names must match include/exclude
include and exclude match the component’s name. With <script setup>, set it via defineOptions({ name }) (or rely on the file name inference where your tooling provides it). A mismatch silently caches everything or nothing.
Router keys decide instance identity
If the router view is keyed by route.fullPath, query-string changes create new cached instances too. Key by the path segments that should create a new instance, or not at all for views that should reuse one instance and react to param changes.
Deactivated DOM is detached, not destroyed
Inactive cached components keep their DOM in a detached container. Heap snapshots will show it as detached DOM — expected for cached views, and a leak only if it exceeds what max allows.
Nested KeepAlives multiply
A KeepAlive inside a cached view keeps its own cache alive while the parent is cached. Bound each level, or avoid nesting caches.
Frequently Asked Questions
Does Vue KeepAlive cause memory leaks?
It retains instances by design. Without max, it retains every instance it has cached, which grows with the number of distinct keys visited and behaves like a leak in long sessions. Setting max, restricting include, and pausing work when deactivated keep it bounded.
What does the max prop on KeepAlive do?
It limits the number of cached component instances. When a new instance would exceed the limit, the least recently used cached instance is unmounted and removed from the cache, releasing its DOM and state.
Do timers stop when a KeepAlive component is deactivated?
No. Deactivation does not unmount the component, so timers, intervals and subscriptions keep running. Stop them in onDeactivated and restart in onActivated.
How do I choose a value for max?
Look at how users actually move between views. If they typically switch between a list, one or two detail views and a dashboard, a max of three to five captures almost all of the instant back-navigation benefit. Measure the retained size of one cached instance of your heaviest view, multiply by max, and confirm the result fits your memory budget for low-end devices; if not, lower max or exclude that view from caching.
Should shallowRef be used for data in cached views?
For large datasets, yes. shallowRef avoids creating deep reactive proxies for every nested object, which reduces both memory and the cost of keeping the data in a cached, inactive instance. Replace the whole value when it changes instead of mutating nested properties.
How do I clear the KeepAlive cache?
Change the key of the <KeepAlive> element (for example to the current user ID) or toggle it with v-if. Vue then unmounts the old KeepAlive and all its cached instances.
Related
- Vue Reactivity and Memory Management — the parent topic
- Pinia and Vuex Store Memory Retention — state that outlives views regardless of caching
- Why React Memory Grows on Every Route Change — the same navigation problem in React
- Framework-Specific Memory Optimization — the section overview