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.

KeepAlive with and without max Without max, the KeepAlive cache holds an instance for every visited detail route, such as orders 1 through 500, each with its DOM, reactive state and watchers. With max set to five, only the five most recently used instances are kept and the least recently used is unmounted when a new one is cached. No max: one cached instance per visited key /orders/1 /orders/2 /orders/3 /orders/500 ~500 × (DOM + state + watchers) max = 5: least recently used is unmounted evicted /orders/496 /orders/497 /orders/498 /orders/499 /orders/500 bounded: memory = 5 instances, whatever the session length

Step-by-Step Fix

  1. 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.
  2. Set max. Add a max prop to <KeepAlive> sized to how many views users realistically switch between (often 3–10). Verification: after visiting more records than max, the number of cached instances stays at max (count onUnmounted calls or inspect with Vue DevTools).
  3. 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.
  4. Pause background work. Move interval, polling, animation and subscription start/stop into onActivated/onDeactivated, keeping onUnmounted cleanup for final teardown. Verification: with a view deactivated, the Network panel shows no polling and the Performance panel no timers from it.
  5. 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.
  6. Reset on logout. Change the KeepAlive’s key (or wrap it in a v-if keyed on the user) so logout discards all cached views. Verification: after logout, no instances from the previous session remain.
Heap after 100 order visits After visiting 100 order detail pages, KeepAlive without max holds about 310 megabytes of heap. With max set to 5 it holds about 48 megabytes. Excluding detail views from caching entirely and caching only the list view holds about 36 megabytes. Heap after 100 order detail visits KeepAlive, no max ~310 MB KeepAlive max = 5 ~48 MB list cached, details excluded ~36 MB

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.

Three KeepAlive properties to verify After visiting many records, the number of cached instances is at most max. While a cached view is inactive, it runs no timers, polling or listeners because they are paused in onDeactivated. After logout, snapshots contain no component instances from the previous session. After visiting many records Cached ≤ max Vue DevTools shows no more inactive instances than max. Inactive is idle No timers, polling or listeners; paused in onDeactivated. Logout clears No instances from the previous session in snapshots.

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.