Олег Марков
Кеширование компонентов во Vue с помощью keep-alive
Введение
Кеширование компонентов через keep-alive во Vue используют, когда нужно сохранять состояние и не пересоздавать компонент при каждом его скрытии и показе. Это особенно актуально для сложных форм, вкладок, списков с фильтрами, многошаговых мастеров и других интерфейсов, где повторный рендеринг и повторные запросы к серверу сильно бьют по производительности и удобству работы.
Вы наверняка сталкивались с ситуацией, когда пользователь переключился на другую вкладку, а при возвращении его данные в форме пропали, или фильтры списка сбросились. Вот как раз для таких случаев и нужен keep-alive. Он позволяет "заморозить" компонент и потом "разморозить" его в том же состоянии, где пользователь его оставил.
Давайте разберемся, как keep-alive работает, где его правильно применять, какие подводные камни есть и как ими управлять.
Что такое keep-alive и как он работает
Общая идея
Компонент <keep-alive> во Vue — это встроенный компонент-обертка, который:
- Не уничтожает вложенный компонент при его выключении из дерева (через
v-if, маршрутизацию и т.д.). - Вместо этого кладет его во внутренний кэш.
- При повторном показе достает компонент из кэша и восстанавливает его состояние.
Смотрите базовый пример:
<template>
<div>
<!-- Здесь мы кэшируем текущий активный компонент -->
<keep-alive>
<!-- is указывает, какой компонент отрендерить -->
<component :is="currentTab"></component>
</keep-alive>
<!-- Переключение вкладок -->
<button @click="currentTab = 'TabA'">Вкладка A</button>
<button @click="currentTab = 'TabB'">Вкладка B</button>
</div>
</template>
<script>
import TabA from './TabA.vue'
import TabB from './TabB.vue'
export default {
name: 'TabsExample',
components: { TabA, TabB },
data() {
return {
// Здесь мы храним имя текущей вкладки
currentTab: 'TabA'
}
}
}
</script>
Важно: когда вы переключаетесь с TabA на TabB, компонент TabA не уничтожается, а просто скрывается и остается в памяти. Его local state (данные в data, положение скролла, состояние ввода и т.п.) сохраняются.
Жизненный цикл компонентов с keep-alive
Когда вы начинаете использовать keep-alive, к обычным хук-функциям жизненного цикла у компонентов добавляются два специальных:
activated— вызывается, когда компонент достают из кэша и снова показывают.deactivated— вызывается, когда компонент убирают с экрана и помещают в кэш.
Компонент при первом показе проходит обычный путь:
created→mounted→activated
При скрытии:
deactivated
(компонент не уничтожается, поэтому хукиbeforeUnmount/unmountedне вызываются)
При повторном показе:
activated
(без повторногоcreatedиmounted)
Давайте разберем это на небольшом примере.
<template>
<div>
<h2>Форма профиля</h2>
<input v-model="name" placeholder="Имя" />
<!-- Здесь мы выводим лог, чтобы увидеть, какие хуки вызываются -->
<pre>{{ log.join('\n') }}</pre>
</div>
</template>
<script>
export default {
name: 'ProfileForm',
data() {
return {
name: '',
log: []
}
},
created() {
// Логируем момент создания компонента
this.log.push('created')
},
mounted() {
// Логируем момент маунта в DOM
this.log.push('mounted')
},
activated() {
// Логируем момент, когда компонент снова активен
this.log.push('activated')
},
deactivated() {
// Логируем момент, когда компонент скрыт, но не уничтожен
this.log.push('deactivated')
},
unmounted() {
// Логируем момент полного удаления компонента
this.log.push('unmounted')
}
}
</script>
Если обернуть ProfileForm в <keep-alive> и переключаться между разными компонентами, вы увидите, что created и mounted вызываются только один раз, а activated / deactivated — при каждом переключении.
Базовое использование keep-alive
Кеширование одних компонентов и пропуск других
Чаще всего keep-alive применяют с динамическими компонентами или с vue-router. Покажу два типовых сценария.
Пример с динамическими компонентами
<template>
<div>
<keep-alive>
<!-- Здесь кэшируется только активный компонент -->
<component :is="current"></component>
</keep-alive>
<button @click="current = 'ListPage'">Список</button>
<button @click="current = 'DetailsPage'">Детали</button>
</div>
</template>
<script>
import ListPage from './ListPage.vue'
import DetailsPage from './DetailsPage.vue'
export default {
name: 'DynamicExample',
components: { ListPage, DetailsPage },
data() {
return {
// Имя текущего компонента
current: 'ListPage'
}
}
}
</script>
ListPageиDetailsPageбудут созданы по одному разу.- При переключении между ними их локальное состояние будет сохраняться.
Пример с vue-router
Смотрите, как это делается с маршрутизатором:
<template>
<div>
<!-- Здесь мы кэшируем текущий маршрут -->
<keep-alive>
<router-view v-slot="{ Component }">
<!-- Оборачиваем сам компонент-страницу -->
<component :is="Component" />
</router-view>
</keep-alive>
</div>
</template>
Здесь:
<router-view v-slot="{ Component }">дает нам фактический компонент страницы.<keep-alive>кэширует все компоненты маршрутов, которые вы уже посещали.- При возврате на предыдущую страницу состояние будет восстановлено.
Управление кэшем keep-alive через props
keep-alive — не просто "включил и забыл". У него есть важные параметры:
includeexcludemax
С помощью них вы можете управлять тем, какие компоненты кэшируются и сколько их максимум.
include — список компонентов, которые нужно кэшировать
include определяет, какие компоненты попадают в кэш. Можно указать:
- строку с именем компонента или паттерном
- массив строк
- регулярное выражение (во Vue 2)
- во Vue 3 — строку или массив; регэксп через
v-bind
Давайте разберем на примере.
<template>
<div>
<!-- Кэшируем только компоненты с именами ListPage и ProfilePage -->
<keep-alive :include="['ListPage', 'ProfilePage']">
<component :is="current"></component>
</keep-alive>
<button @click="current = 'ListPage'">Список</button>
<button @click="current = 'ProfilePage'">Профиль</button>
<button @click="current = 'SettingsPage'">Настройки</button>
</div>
</template>
<script>
import ListPage from './ListPage.vue'
import ProfilePage from './ProfilePage.vue'
import SettingsPage from './SettingsPage.vue'
export default {
name: 'IncludeExample',
components: { ListPage, ProfilePage, SettingsPage },
data() {
return {
current: 'ListPage'
}
}
}
</script>
Важно: include и exclude работают по name компонента, а не по имени, под которым вы его импортировали. То есть внутри самого компонента должно быть:
export default {
name: 'ListPage', // Это имя должно совпадать с include
// ...
}
Если имя не указано, keep-alive не сможет его корректно фильтровать по include / exclude.
exclude — список компонентов, которые НЕ нужно кэшировать
Работает наоборот: вы указываете, какие компоненты нужно игнорировать и не помещать в кэш.
<template>
<div>
<!-- Кэшируем все компоненты, кроме SettingsPage -->
<keep-alive exclude="SettingsPage">
<component :is="current"></component>
</keep-alive>
<button @click="current = 'ListPage'">Список</button>
<button @click="current = 'ProfilePage'">Профиль</button>
<button @click="current = 'SettingsPage'">Настройки</button>
</div>
</template>
Здесь:
ListPageиProfilePageбудут кэшироваться.SettingsPageпри каждом показе будет создаваться заново и уничтожаться при переключении.
max — ограничение размера кэша
max задает максимальное количество компонентных экземпляров, которые keep-alive может хранить. Если лимит превышен, самый "старый" (долго неактивный) компонент будет удален.
<template>
<div>
<!-- Храним в кэше максимум 2 компонента -->
<keep-alive :max="2">
<component :is="current"></component>
</keep-alive>
<button @click="current = 'PageA'">A</button>
<button @click="current = 'PageB'">B</button>
<button @click="current = 'PageC'">C</button>
</div>
</template>
<script>
import PageA from './PageA.vue'
import PageB from './PageB.vue'
import PageC from './PageC.vue'
export default {
name: 'MaxExample',
components: { PageA, PageB, PageC },
data() {
return {
current: 'PageA'
}
}
}
</script>
Давайте посмотрим, как это будет работать:
- Открываете
PageA→ кэш: [A]. - Переключаетесь на
PageB→ кэш: [A, B]. - Переключаетесь на
PageC→ кэш был полон (2 элемента), поэтому один нужно удалить. - Удаляется самый давно неактивный компонент (в данном случае
PageA).
Если вы снова перейдете на PageA, он создастся заново.
Механика kеу и кэширования: как keep-alive понимает, что кэшировать
Чтобы компонент корректно кэшировался, keep-alive должен уметь его однозначно идентифицировать. Он делает это по комбинации:
nameкомпонента- или
key, если он явно задан
Если вы используете router-view, роутер сам устанавливает уникальный key. Но иногда нужно управлять этим вручную.
Использование key с dynamic components
Например, вы хотите кэшировать разные варианты одного и того же компонента с разными параметрами. В этом случае полезно задать key.
<template>
<div>
<keep-alive>
<!-- Здесь мы формируем key из имени и типа фильтра -->
<ListPage
v-if="current === 'list'"
:filter="filter"
:key="`list-${filter}`"
/>
<DetailsPage
v-else
:item-id="selectedId"
:key="`details-${selectedId}`"
/>
</keep-alive>
</div>
</template>
<script>
import ListPage from './ListPage.vue'
import DetailsPage from './DetailsPage.vue'
export default {
name: 'KeyExample',
components: { ListPage, DetailsPage },
data() {
return {
current: 'list',
filter: 'active', // Текущий фильтр
selectedId: null // Текущий выбранный элемент
}
}
}
</script>
Комментарии к этому коду:
- Для списка с разными фильтрами (
active,archivedи т.п.) создаются разные ключи:list-active,list-archived. - keep-alive будет хранить несколько версий одного и того же компонента
ListPage, каждая — со своим состоянием. - Это удобно, если вы хотите, чтобы состояние формы фильтра и позиция скролла сохранялись отдельно для каждого фильтра.
Жизненные циклы activated и deactivated на практике
Теперь давайте посмотрим, как правильно использовать хуки activated и deactivated в реальных задачах.
Сценарий 1: Остановка и перезапуск таймеров
Если в компоненте есть интервалы, таймеры или подписки на внешние источники, вполне логично при скрытии компонента их приостанавливать, а при возврате — возобновлять.
<template>
<div>
<h2>Онлайн статус</h2>
<p>Последняя проверка - {{ lastCheck }}</p>
</div>
</template>
<script>
export default {
name: 'StatusChecker',
data() {
return {
lastCheck: null,
intervalId: null
}
},
mounted() {
// Запускаем таймер при первом маунте
this.startTimer()
},
activated() {
// Возобновляем таймер при активации из кэша
this.startTimer()
},
deactivated() {
// Останавливаем таймер при деактивации
this.stopTimer()
},
beforeUnmount() {
// Очищаем таймер при полном уничтожении компонента
this.stopTimer()
},
methods: {
startTimer() {
// Если таймер уже идет - выходим
if (this.intervalId) return
// Здесь мы запускаем периодическую проверку
this.intervalId = setInterval(() => {
this.lastCheck = new Date().toLocaleTimeString()
}, 2000)
},
stopTimer() {
// Останавливаем таймер, если он есть
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
}
}
</script>
Так вы не будете тратить ресурсы на таймер, когда компонент скрыт, но при этом сохраните текущее состояние.
Сценарий 2: Отложенная загрузка данных
Иногда вы хотите загружать данные не при первом показе, а каждый раз, когда пользователь возвращается на страницу. С keep-alive это можно сделать через activated.
<template>
<div>
<h2>Список задач</h2>
<button @click="reload">Обновить</button>
<ul>
<li v-for="task in tasks" :key="task.id">
{{ task.title }}
</li>
</ul>
<p v-if="loading">Загрузка...</p>
</div>
</template>
<script>
export default {
name: 'TasksPage',
data() {
return {
tasks: [],
loading: false
}
},
activated() {
// При каждом возврате на страницу запускаем обновление
this.reload()
},
methods: {
async reload() {
// Здесь мы отмечаем, что идет загрузка
this.loading = true
try {
// Имитация запроса к API
const response = await this.fakeApi()
this.tasks = response
} finally {
// В любом случае сбрасываем флаг загрузки
this.loading = false
}
},
fakeApi() {
// Здесь мы возвращаем промис с имитацией задержки
return new Promise(resolve => {
setTimeout(() => {
resolve([
{ id: 1, title: 'Сделать отчёт' },
{ id: 2, title: 'Позвонить клиенту' }
])
}, 1000)
})
}
}
}
</script>
Если вам, наоборот, нужно загружать данные только один раз при первом входе — поместите запрос в mounted и не трогайте activated.
Типовые сценарии использования keep-alive
Формы и многошаговые мастера
Один из самых частых кейсов — формы, где пользователь может уйти на другую страницу или вкладку и вернуться позже.
<template>
<div>
<keep-alive>
<component :is="step"></component>
</keep-alive>
<button @click="step = 'Step1'">Шаг 1</button>
<button @click="step = 'Step2'">Шаг 2</button>
<button @click="step = 'Step3'">Шаг 3</button>
</div>
</template>
<script>
import Step1 from './Step1.vue'
import Step2 from './Step2.vue'
import Step3 from './Step3.vue'
export default {
name: 'WizardExample',
components: { Step1, Step2, Step3 },
data() {
return {
step: 'Step1'
}
}
}
</script>
Каждый шаг мастера:
- хранит свои данные у себя в
data; - при переключении шагов состояние каждого остается в кэше;
created/mountedу каждого шага вызываются только один раз.
Вкладки (tabs) и панельные интерфейсы
Вкладки — ещё один удобный сценарий, когда важно сохранять состояние каждой вкладки отдельно.
<template>
<div>
<div class="tabs">
<button @click="tab = 'UsersTab'">Пользователи</button>
<button @click="tab = 'OrdersTab'">Заказы</button>
<button @click="tab = 'ReportsTab'">Отчеты</button>
</div>
<!-- Здесь мы кэшируем вкладки -->
<keep-alive>
<component :is="tab"></component>
</keep-alive>
</div>
</template>
<script>
import UsersTab from './UsersTab.vue'
import OrdersTab from './OrdersTab.vue'
import ReportsTab from './ReportsTab.vue'
export default {
name: 'TabsPage',
components: { UsersTab, OrdersTab, ReportsTab },
data() {
return {
tab: 'UsersTab'
}
}
}
</script>
Например:
- Вкладка "Заказы" может хранить состояние фильтров и номера страницы.
- Вкладка "Отчеты" может держать выбранный период и состояние загруженных данных.
Где keep-alive использовать не стоит
Важно понимать, что keep-alive — не универсальное решение для всех компонентов. Есть случаи, когда его использование только навредит.
1. Одноразовые страницы без сложного состояния
Если страница достаточно простая:
- нет тяжелых запросов;
- нет сложной формы;
- нет дорогих вычислений;
то кэшировать её бессмысленно. Вы только увеличите потребление памяти, не получив заметной выгоды.
2. Компоненты с большим объемом данных в памяти
Например:
- сложные таблицы с тысячами строк;
- визуализация больших наборов данных;
- объемные графики.
Если таких страниц много и вы кэшируете их все, кэш может сильно разрастаться и нагружать память браузера. В таких случаях:
- либо ограничивайте число кэшируемых экземпляров через
max; - либо кэшируйте только действительно нужные разделы приложения.
3. Компоненты, которые должны всегда обновляться
Иногда компонент должен каждый раз создаваться заново:
- страницы, где вы хотите сбрасывать форму при каждом посещении;
- страницы, завязанные на одноразовые токены или одноразовые состояния.
В этом случае их нужно исключить через exclude или просто не оборачивать в keep-alive.
Особенности работы keep-alive с vue-router
Чаще всего keep-alive используют именно с маршрутизатором. Здесь есть несколько нюансов, которые полезно учитывать.
Кэширование только отдельных маршрутов
Вы можете кэшировать только определенные страницы на основе полей meta в конфигурации роутера.
Например, в настройке маршрутов:
// router.js
export default [
{
path: '/list',
name: 'ListPage',
component: () => import('./pages/ListPage.vue'),
meta: { keepAlive: true } // Этот маршрут нужно кэшировать
},
{
path: '/details/:id',
name: 'DetailsPage',
component: () => import('./pages/DetailsPage.vue'),
meta: { keepAlive: false } // Этот маршрут не кэшируем
}
]
А в корневом компоненте приложения:
<template>
<div>
<keep-alive>
<!-- Здесь мы кэшируем только те маршруты, у которых meta.keepAlive === true -->
<router-view v-slot="{ Component, route }">
<component
:is="Component"
v-if="route.meta.keepAlive"
/>
</router-view>
</keep-alive>
<!-- А эти маршруты не кэшируем -->
<router-view v-slot="{ Component, route }">
<component
:is="Component"
v-if="!route.meta.keepAlive"
/>
</router-view>
</div>
</template>
Комментарии:
- Первый
<router-view>обернут в<keep-alive>и показывает только маршруты, помеченныеkeepAlive: true. - Второй
<router-view>рендерит остальные маршруты без кэширования.
Такой подход удобно использовать, если нужно четко контролировать, какие страницы сохраняют состояние.
Использование name маршрута вместо name компонента
Еще один частый прием — совпадение имени маршрута и компонента и использование include по имени:
<template>
<div>
<!-- Кэшируем только указанные страницы по имени компонента -->
<keep-alive include="ListPage,ProfilePage">
<router-view />
</keep-alive>
</div>
</template>
Для этого нужно, чтобы:
- компонент имел
name: 'ListPage'; - у маршрута было
name: 'ListPage'(это не обязательно, но полезно для единообразия).
Частые ошибки и подводные камни
Ошибка 1: Неуказанное имя компонента
Очень частая ситуация — попытка использовать include / exclude, когда у самого компонента не задан name.
export default {
// name не указан
data() {
return {}
}
}
В этом случае keep-alive не сможет фильтровать компонент. Чтобы исправить, достаточно:
export default {
name: 'MyPage', // Обязательно указываем имя
data() {
return {}
}
}
Ошибка 2: Ожидание вызова mounted при каждом возврате
Некоторые разработчики ожидают, что при каждом возвращении к кэшируемому компоненту сработает mounted. На самом деле:
mountedвызывается один раз — при первом монтировании.- При возврате из кэша вызывается только
activated.
Если вам нужно выполнять какую-то логику при каждом возврате к компоненту, переместите её в activated.
Ошибка 3: Протечки памяти из-за забытых подписок
Если у компонента есть:
- подписки на WebSocket;
- прослушивание
window.addEventListener; - сторонние библиотеки, привязанные к DOM;
и вы не освобождаете эти ресурсы в deactivated и beforeUnmount, то при длительной работе приложения кэш может разрастись и вызвать утечки.
Решение:
- отписывайтесь от подписок в
deactivated(если при следующей активации подписка должна возобновиться); - гарантированно очищайте ресурсы в
beforeUnmount.
Ошибка 4: Кэширование слишком большого количества страниц
Если keep-alive оборачивает весь router-view и в приложении десятки тяжелых страниц, есть риск переполнить память:
- используйте
max, чтобы ограничить число кэшируемых компонентов; - либо выборочно кэшируйте только важные страницы.
Заключение
Кеширование компонентов через keep-alive во Vue — это инструмент, который помогает:
- сохранять состояние компонентов между переключениями;
- уменьшать количество повторных запросов к серверу;
- повышать отзывчивость интерфейса;
- оптимизировать работу сложных форм и многостраничных интерфейсов.
Но при этом важно:
- осознанно выбирать, какие компоненты кэшировать, а какие нет;
- не забывать про специальные хуки
activated/deactivatedдля управления побочными эффектами; - указывать
nameу компонентов, если вы используетеinclude/exclude; - следить за объемом кэша через
maxи за потреблением памяти.
Используйте keep-alive как точечный инструмент оптимизации, а не как глобальный "ускоритель всего". Тогда он будет работать в вашу пользу и не создавать неожиданных проблем.
Частозадаваемые технические вопросы по теме и ответы
Как принудительно очистить кэш одного компонента, не трогая остальные
Сам keep-alive не предоставляет публичного API для удаления одного элемента кэша. На практике используют управление key:
- Оборачиваете компонент в
keep-aliveкак обычно. - Храните во внешнем состоянии версию ключа.
- Чтобы сбросить кэш, меняете
key, и Vue создаст новый экземпляр.
Пример:
<keep-alive>
<MyForm :key="formVersion" />
</keep-alive>
<button @click="formVersion++">Сбросить кэш формы</button>
Так вы удаляете старый экземпляр из кэша и создаете новый.
Почему activated не вызывается у компонента внутри keep-alive
Чаще всего причины две:
- Компонент реально не кэшируется, потому что:
- не подходит под
include/ попадает подexclude; - не указан
nameу компонента.
- не подходит под
- Компонент вложен слишком глубоко и фактически не оборачивается
keep-alive.
Проверьте:
- что
<keep-alive>находится над тем компонентом, где вы ожидаетеactivated; - что компонент непосредственно рендерится внутри
<keep-alive>черезcomponentилиrouter-view; - что имя компонента (
name) совпадает с правиламиinclude/exclude.
Как временно отключить keep-alive для некоторого маршрута
Самый простой способ — условно рендерить router-view внутри и вне keep-alive:
- В метаданных маршрута добавляете флаг, например,
noCache: true. - В корневом компоненте рендерите:
<keep-alive>
<router-view v-slot="{ Component, route }">
<component v-if="!route.meta.noCache" :is="Component" />
</router-view>
</keep-alive>
<router-view v-slot="{ Component, route }">
<component v-if="route.meta.noCache" :is="Component" />
</router-view>
Так вы явно разделяете кэшируемые и некэшируемые маршруты.
Как работать с keep-alive во Vue 3 с Composition API
С точки зрения keep-alive разницы почти нет. Отличие — в том, как вы объявляете хуки:
import { onActivated, onDeactivated, onMounted } from 'vue'
export default {
name: 'MyPage',
setup() {
onMounted(() => {
// Логика первого маунта
})
onActivated(() => {
// Логика при каждом возврате
})
onDeactivated(() => {
// Логика при скрытии и кэшировании
})
}
}
Остальное — такое же: include, exclude, max работают на уровне <keep-alive>.
Можно ли кэшировать сразу несколько уровней вложенности компонентов
Можно, но keep-alive работает только на том уровне, на котором он оборачивает vnode. Если вы хотите кэшировать и родительский компонент, и часть дочерних независимо:
- Оборачиваете
router-viewили динамический компонент в один<keep-alive>. - Внутри кэшируемого компонента можете использовать еще один
<keep-alive>для своих дочерних динамических частей.
Главное — не переусердствовать с вложенностью, чтобы не усложнить управление состоянием и не создать лишнюю нагрузку по памяти.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев