Как оптимизировать производительность Vue-приложения?

SeniorVue.js #vue #performance #optimization

Вопрос

Какие есть техники оптимизации производительности 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)?