Frontend Development 11 min read

Dynamic Tab Caching with Vue keep-alive

Vue’s keep-alive component enables dynamic tab caching in B‑end applications by storing component VNodes in an internal LRU cache, preserving state across route switches, supporting include/exclude patterns, router‑meta flags, Vuex‑driven lists, and custom strategies for nested routes to prevent stale data and excess API calls.

DeWu Technology
DeWu Technology
DeWu Technology
Dynamic Tab Caching with Vue keep-alive

In B‑end applications, tabs that behave like browser tabs are often required. To keep the state of each tab when users switch routes, Vue's keep-alive component can be used for dynamic caching.

About keep-alive : it is an abstract component that does not render a DOM node and does not create a parent‑child relationship with its children. It simply stores component VNodes in an internal cache.

The main benefits are:

Component state is retained in memory, preventing repeated DOM rendering.

Reduces unnecessary API calls and server load.

Enables both route‑level and component‑level caching.

When a component is wrapped by keep-alive , the activated lifecycle hook is added. The typical lifecycle order is created → mounted → activated on first entry, and deactivated on exit. Subsequent navigation triggers only activated .

Rendering logic : during render, keep-alive obtains the first child component via getFirstComponentChild , checks the include and exclude patterns, and decides whether to cache the VNode.

const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)

If the component name does not match the include pattern or matches exclude , the VNode is returned without caching.

// check pattern
const name = getComponentName(componentOptions)
const { include, exclude } = this
if ((include && (!name || !matches(include, name))) ||
    (exclude && name && matches(exclude, name))) {
  return vnode
}

When a match occurs, the VNode is stored in cache with a key. The cache follows an LRU (Least Recently Used) strategy: the accessed key is moved to the end of the keys array, and when the cache size exceeds max , the oldest entry is pruned.

const { cache, keys } = this
const key = vnode.key == null
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key
if (cache[key]) {
  vnode.componentInstance = cache[key].componentInstance
  remove(keys, key)
  keys.push(key)
} else {
  cache[key] = vnode
  keys.push(key)
  if (this.max && keys.length > parseInt(this.max)) {
    pruneCacheEntry(cache, keys[0], keys, this._vnode)
  }
}

The pruneCacheEntry function destroys the component instance and removes its key.

function pruneCacheEntry(cache, key, keys, current) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

Caching schemes :

Full‑page cache using a keepAlive flag in router.meta :

<keep-alive>
  <router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"/>

Dynamic component cache with Vuex include list:

<keep-alive :include="$store.state.keepAlive.cachedView">
  <router-view/>
</keep-alive>

Vuex store maintains cachedViews and updates it on route changes.

const state = { cachedViews: [] }
const mutations = {
  ADD_VIEWS: (state, view) => {
    if (!state.cachedViews.includes(view.name)) state.cachedViews.push(view.name)
  },
  DEL_CACHED_VIEW: (state, view) => {
    const i = state.cachedViews.indexOf(view.name)
    i > -1 && state.cachedViews.splice(i, 1)
  }
}
const actions = {
  addCachedView({ commit }, view) { commit('ADD_VIEWS', view) },
  deleteCachedView({ commit }, view) { commit('DEL_CACHED_VIEW', view) }
}
export default { namespaced: true, state, mutations, actions }

When nested routes are used, the default cache may fail. Three solutions are proposed:

Keep menu hierarchy nested but flatten routes for registration.

Expose the internal cache object globally (e.g., via Vuex) so child components can be cached without caching the parent.

Cache by route name instead of component name, by overriding getComponentName and using the route name as the cache key.

import Vue from 'vue'
const cache = {}
const keys = []
export default Object.assign({}, Vue.options.components.KeepAlive, {
  name: 'NewKeepAlive',
  created() { this.cache = cache; this.keys = keys },
})
function getComponentName(opts) { return this.$route.name }

These approaches allow precise control over which views are cached, solving issues such as stale data after closing a tab and excessive network requests.

References: Vue 2 API keep-alive, dynamic async components guide, and several community articles.

cachingSPAVue.jsdynamic componentskeep-aliveLRURouter
DeWu Technology
Written by

DeWu Technology

A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.