Оптимизация: 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.- Длинные списки лечит виртуализация — рендер только видимых строк; сначала измеряйте, потом оптимизируйте.