Как оптимизировать производительность Vue-приложения?
Вопрос
Какие есть техники оптимизации производительности Vue-приложения: v-once/v-memo, <KeepAlive>, ленивая загрузка компонентов, виртуализация списков?
Короткий ответ
Основные рычаги: v-once и v-memo пропускают повторный рендеринг статичных или редко меняющихся участков шаблона; <KeepAlive> сохраняет состояние и DOM неактивных компонентов вместо их пересоздания при переключении; defineAsyncComponent разбивает бандл и грузит тяжёлые компоненты по требованию; виртуализация рендерит только видимые элементы длинного списка, а не все сразу.
Подробный ответ
v-once. Рендерит элемент/компонент один раз и больше никогда не обновляет, даже если реактивные данные внутри изменились. Подходит для заведомо статичного содержимого (например, текст, который вычисляется один раз при монтировании и больше не должен меняться) — Vue исключает этот узел из последующих обновлений, экономя на diffing.
v-memo. Принимает массив зависимостей: v-memo="[valueA, valueB]". Vue пропускает повторный рендер поддерева, если значения в массиве не изменились с прошлого рендера — по сути, аналог React.memo/мемоизации для куска шаблона. Полезно для больших списков, где перерисовка каждого элемента при любом изменении родителя дорога, а реально меняется редко.
<KeepAlive>. Оборачивает динамический компонент (<component :is="..."> или содержимое <router-view>) и кэширует экземпляры неактивных компонентов вместо их уничтожения при переключении (v-if/смена маршрута по умолчанию демонтирует компонент полностью). Это сохраняет состояние компонента (например, введённый в форму текст, скролл списка) и избавляет от повторной инициализации/повторных запросов при возврате назад. Даёт хуки onActivated/onDeactivated вместо обычных onMounted/onUnmounted. Поддерживает include/exclude/max для контроля, что и сколько кэшировать.
Ленивая загрузка (defineAsyncComponent). Тяжёлые компоненты (модалки, редакторы, графики), не нужные при первом рендере, оборачиваются в defineAsyncComponent(() => import('./Heavy.vue')). Компилятор (Vite/webpack) разбивает такой импорт в отдельный chunk, который подгружается только когда компонент действительно понадобился — уменьшает начальный размер бандла и время до интерактивности (TTI). Аналогично работает ленивая загрузка компонентов маршрутов в Vue Router.
Виртуализация длинных списков. При рендере тысяч <li>/строк таблицы даже эффективный diffing Vue не спасает — DOM с тысячами узлов сам по себе дорог (layout, paint, память). Виртуализация рендерит в DOM только те элементы, что видны во вьюпорте (плюс небольшой буфер), подменяя остальное пустым пространством (spacer) нужной высоты, и подгружает/выгружает элементы при скролле. Реализуется через библиотеки (vue-virtual-scroller, @tanstack/vue-virtual) или вручную через IntersectionObserver/расчёт видимого диапазона по scrollTop.
Пример
<script setup>
import { ref, defineAsyncComponent } from 'vue'
// компонент грузится отдельным chunk'ом только при необходимости
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
const showChart = ref(false)
const staticLabel = 'Это вычислено один раз' // для v-once
const rows = ref(
Array.from({ length: 1000 }, (_, i) => ({ id: i, value: i * 2 }))
)
</script>
<template>
<!-- v-once: рендерится один раз и больше не обновляется -->
<h3 v-once>{{ staticLabel }}</h3>
<!-- v-memo: перерисовать строку только если изменился именно её value -->
<ul>
<li v-for="row in rows" :key="row.id" v-memo="[row.value]">
{{ row.id }}: {{ row.value }}
</li>
</ul>
<!-- KeepAlive: состояние компонента сохраняется при повторном показе -->
<button @click="showChart = !showChart">Переключить график</button>
<KeepAlive>
<HeavyChart v-if="showChart" />
</KeepAlive>
</template>
Дополнительные вопросы
- Чем
v-memoотличается от оборачивания части шаблона в отдельный компонент сcomputed-пропсами? - Что произойдёт с состоянием формы при переключении вкладок, если не использовать
<KeepAlive>? - Как отследить реальные узкие места рендеринга через Vue Devtools (component render timings)?