Оптимизация: v-memo, computed, ленивость, KeepAlive

Берём знания о diffing из прошлого урока и применяем их: убираем лишние пересчёты, ререндеры и ненужно загруженный код.

Оптимизация во Vue — это в первую очередь не «писать быстрый код», а не давать фреймворку делать лишнюю работу: лишние пересчёты, лишние ререндеры, лишние DOM-узлы и лишний загруженный JavaScript.

В прошлом уроке мы выяснили: Vue и так умён и патчит только различия. Поэтому преждевременная оптимизация здесь вредна — большинство приложений быстры из коробки. Но на больших списках, тяжёлых вычислениях и крупных бандлах появляются реальные узкие места. Этот урок — про конкретные инструменты для каждого случая и про то, когда их применять, а когда нет.

computed — кэш, про который забывают

Самая частая и самая дешёвая оптимизация — использовать computed вместо метода. Computed-свойство кэширует результат и пересчитывается только когда меняются его реактивные зависимости. Метод же выполняется заново при каждом рендере, даже если входные данные не менялись.

// Демонстрация кэша: пересчёт только при смене входа
function makeComputed(fn) {
  let cache, lastInput, has = false;
  return (input) => {
    if (has && input === lastInput) return cache; // отдаём кэш
    cache = fn(input); lastInput = input; has = true;
    return cache;
  };
}
let calls = 0;
const total = makeComputed(arr => { calls++; return arr.length; });
const list = [1, 2, 3];
total(list); total(list); total(list);   // вход не менялся
console.log("Реальных вычислений:", calls);

Вывод:

Реальных вычислений: 1

Правило простое: если значение выводится из других реактивных данных (отфильтрованный список, сумма, форматированная строка) — это computed. Метод оставляйте для действий по событию (клик, отправка формы).

v-memo — заморозка поддерева

Иногда даже патчинг различий — это много, если узлов тысячи. Директива v-memo говорит Vue: «пропусти обновление этого поддерева, пока перечисленные значения не изменились». Это ручной рубильник для горячих мест, чаще всего внутри больших v-for.

<!-- Строка обновляется ТОЛЬКО если изменился её id или флаг selected -->
<div
  v-for="row in rows"
  :key="row.id"
  v-memo="[row.id, row.selected]"
>
  {{ row.label }} — {{ row.selected ? 'выбрано' : '' }}
</div>

Пока row.id и row.selected те же — Vue полностью пропускает этот узел при diffing, не сравнивая его внутренности. Выигрыш заметен на таблицах в тысячи строк, где меняется лишь несколько. Важно: v-memo — это острый инструмент. На обычных компонентах он не нужен и даже вреден (добавляет накладные расходы на сравнение массива зависимостей). Применяйте только когда профайлер показал проблему.

Ленивые компоненты — грузим код по требованию

Не весь код нужен сразу. Модальное окно настроек, тяжёлый редактор, страница админки — всё это можно вынести в отдельный кусок бандла и загрузить только когда понадобится. За это отвечает defineAsyncComponent.

// import() возвращает Promise -> сборщик выделит компонент в отдельный chunk
import { defineAsyncComponent } from "vue";

const SettingsModal = defineAsyncComponent(() =>
  import("./SettingsModal.vue")
);
// Файл SettingsModal.js скачается только при первом рендере компонента

Результат: начальный бандл меньше, приложение открывается быстрее, а редкий код подгружается в фоне при первом обращении. На уровне маршрутов то же делает vue-router: component: () => import('./AdminPage.vue') грузит страницу только при переходе на неё.

KeepAlive — сохраняем состояние вместо пересоздания

По умолчанию при переключении v-if или маршрута компонент уничтожается, а при возврате создаётся с нуля — теряются введённые данные, позиция прокрутки, открытые вкладки. <KeepAlive> кэширует уже смонтированные компоненты в памяти и при возврате оживляет их, а не строит заново.

<!-- Состояние вкладок сохраняется при переключении -->
<KeepAlive>
  <component :is="currentTab" />
</KeepAlive>

<!-- Кэшировать только перечисленные, чтобы не раздувать память -->
<KeepAlive :include="['ProfileTab', 'SettingsTab']">
  <component :is="currentTab" />
</KeepAlive>

Вместо обычных mounted/unmounted у закэшированных компонентов срабатывают хуки onActivated и onDeactivated — туда вешают паузу таймеров и видео, обновление данных при возврате. Не оборачивайте в KeepAlive всё подряд: кэш живёт в памяти, и десятки тяжёлых компонентов её съедят.

Виртуализация длинных списков

Список на 10 000 строк убьёт страницу не из-за Vue, а из-за того, что в DOM окажется 10 000 узлов — браузеру тяжело их хранить и раскладывать. Решение радикальное: рендерить только видимые строки. Это называется виртуализация (или «окно»). В DOM в любой момент существует, скажем, 20 строк, попавших в видимую область; при прокрутке они переиспользуются и подменяют содержимое.

// Суть виртуализации: по позиции скролла считаем видимый диапазон
const rowHeight = 40, viewport = 400, total = 10000;
function visibleRange(scrollTop) {
  const start = Math.floor(scrollTop / rowHeight);
  const count = Math.ceil(viewport / rowHeight);
  return { start, end: start + count };
}
console.log(visibleRange(0));      // верх списка
console.log(visibleRange(8000));   // прокрутили вниз

Вывод:

{ start: 0, end: 10 }
{ start: 200, end: 210 }

Из 10 000 строк в DOM живут ~10. Самому это писать не нужно — есть готовые библиотеки (например, vue-virtual-scroller), но понимать принцип важно: вы меняете количество DOM-узлов с «все» на «видимые», и тормоза исчезают.

Как это работает под капотом

Все эти приёмы бьют в одну из четырёх осей лишней работы. computed убирает лишние вычисления за счёт кэша по зависимостям. v-memo убирает лишний diffing, замораживая поддерево. Ленивые компоненты убирают лишний загруженный код, разбивая бандл на чанки через динамический import(). KeepAlive и виртуализация убирают лишние DOM-операции: первая — пересоздание узлов, вторая — само их количество.

Понимание оси помогает выбрать инструмент. Тормозит при наборе в поле, пока пересчитывается список? Скорее всего, это вычисление — смотрите в сторону computed. Тормозит прокрутка таблицы? Это либо число узлов (виртуализация), либо объём патчинга (v-memo). Долго открывается приложение? Это размер бандла — ленивая загрузка.

Частые ошибки

  • Оптимизация без измерения. Сначала Vue DevTools и вкладка Performance в браузере, потом инструмент. Иначе вы усложняете код там, где проблемы не было.
  • Метод вместо computed для производных данных. Метод пересчитывается на каждом рендере — самый частый источник «непонятных тормозов» в фильтруемых списках.
  • v-memo на всём подряд. На обычных компонентах он добавляет накладные расходы и усложняет код, не давая выигрыша. Только горячие большие v-for.
  • KeepAlive поверх всего приложения. Память не бесконечна; кэшируйте через :include точечно и не забывайте про onDeactivated для пауз.
  • Рендер тысяч строк без виртуализации. Никакой v-memo не спасёт, если в DOM физически 10 000 узлов — нужно уменьшать их число, а не ускорять обновление.

Итоги

  • computed кэширует производные значения и пересчитывается только при смене зависимостей — это базовая и почти бесплатная оптимизация.
  • v-memo замораживает поддерево в diffing, пока не изменятся указанные значения; нужен лишь в горячих больших списках.
  • defineAsyncComponent и () => import() разбивают бандл на чанки и грузят редкий код по требованию.
  • KeepAlive сохраняет состояние компонентов вместо пересоздания; используйте :include и хуки onActivated/onDeactivated.
  • Длинные списки лечит виртуализация — рендер только видимых строк; сначала измеряйте, потом оптимизируйте.
Проверьте себя
1. Чем computed-свойство выгоднее обычного метода для вычисления отфильтрованного списка?
AМетод нельзя вызывать из шаблона
Bcomputed кэширует результат и пересчитывается только при изменении своих реактивных зависимостей, а метод выполняется заново на каждом рендере
CМетод всегда возвращает undefined в шаблоне
Dcomputed выполняется на сервере, а метод в браузере
2. В каком случае оправдано применить v-memo?
AНа каждом компоненте приложения для подстраховки
BНа корневом компоненте, чтобы заморозить всю страницу
CВ горячем большом v-for (таблица на тысячи строк), где меняются лишь немногие строки
DНа любом computed-свойстве вместо кэша
3. Что даёт оборачивание переключаемых вкладок в <KeepAlive>?
AКомпоненты кэшируются в памяти и при возврате оживляются с сохранённым состоянием вместо создания заново
BВсе вкладки рендерятся одновременно и всегда видны
CVue перестаёт отслеживать реактивность внутри них
DБандл автоматически разбивается на чанки
4. Почему список на 10 000 строк тормозит и как это правильно лечить?
AВиноват diffing Vue; лечится отключением реактивности
BВ реальном DOM оказывается 10 000 узлов — браузеру тяжело; лечится виртуализацией: рендером только видимых строк
CСписок тормозит из-за computed; нужно заменить его на метод
DПроблема в размере бандла; поможет defineAsyncComponent