26. Производительность
⚡ Урок 27: Производительность во Vue 3
Заголовок раздела «⚡ Урок 27: Производительность во Vue 3»Vue 3 уже очень быстрый из коробки — компилятор шаблонов делает оптимизации автоматически. Но когда у тебя тысячи элементов в списке, сложные вычисления или дерево компонентов глубиной 20 уровней — нужно знать инструменты для ручной оптимизации. 🚀
📊 Как измерить производительность?
Заголовок раздела «📊 Как измерить производительность?»Прежде чем оптимизировать — измерь. Не угадывай, где узкое место!
// Vue DevTools — вкладка Performance// Показывает время рендера каждого компонента
// Профайлер в браузереperformance.mark('render-start');// ... код ...performance.mark('render-end');performance.measure('render', 'render-start', 'render-end');
// Vue 3 встроенный профайлер (dev mode)const app = createApp(App);app.config.performance = true; // включить трассировку в DevTools🎯 v-once — рендеринг один раз
Заголовок раздела «🎯 v-once — рендеринг один раз»Директива v-once указывает Vue: «этот элемент никогда не изменится — не трать время на его отслеживание».
<template> <!-- Статичный контент, который никогда не меняется --> <header v-once> <h1>{{ appTitle }}</h1> <nav> <a v-for="link in staticLinks" :key="link.href" :href="link.href"> {{ link.label }} </a> </nav> </header>
<!-- Счётчик посещений — меняется, поэтому без v-once --> <p>Посещений: {{ visitCount }}</p></template>
<script setup lang="ts">const appTitle = 'Мой Магазин';const staticLinks = [ { href: '/', label: 'Главная' }, { href: '/catalog', label: 'Каталог' }, { href: '/about', label: 'О нас' },];const visitCount = ref(0);</script>Vue скомпилирует v-once в статичный VNode и пропустит его при перерендере. Отлично для шапки, подвала, статичных баннеров.
📌 v-memo — мемоизация шаблона
Заголовок раздела «📌 v-memo — мемоизация шаблона»v-memo — более гибкий v-once. Принимает массив зависимостей и пропускает ререндер, пока зависимости не изменились.
<template> <!-- Список с тысячами элементов --> <ul> <li v-for="item in hugeList" :key="item.id" v-memo="[item.id, item.selected, item.label]" > <!-- Ререндер только если изменились id, selected или label --> <span>{{ item.label }}</span> <input type="checkbox" :checked="item.selected" /> <ExpensiveSubComponent :data="item.details" /> </li> </ul></template><!-- Особенно полезно с v-for + частичными обновлениями --><template> <div v-for="row in rows" :key="row.id" v-memo="[row.id === selectedId]"> <!-- Ререндерится только при смене selected-строки --> <TableRow :row="row" :selected="row.id === selectedId" /> </div></template>⚠️ Важно: v-memo с пустым массивом [] — то же самое что v-once.
🔬 shallowRef и shallowReactive
Заголовок раздела «🔬 shallowRef и shallowReactive»По умолчанию ref и reactive делают объект глубоко реактивным — следят за каждым вложенным свойством. Это дорого для больших объектов!
import { ref, shallowRef, reactive, shallowReactive } from 'vue';
// ref — глубокая реактивность (следит за всеми вложенными свойствами)const deepState = ref({ user: { profile: { settings: { theme: 'dark' } } }});// Это тоже реактивно:deepState.value.user.profile.settings.theme = 'light'; // ✅ триггерит обновление
// shallowRef — реактивна только смена .value целикомconst shallowState = shallowRef({ user: { profile: { settings: { theme: 'dark' } } }});// Это НЕ реактивно:shallowState.value.user.profile.settings.theme = 'light'; // ❌ НЕ триггерит// Это реактивно:shallowState.value = { ...shallowState.value }; // ✅ триггерит// shallowReactive — реактивен только первый уровеньconst state = shallowReactive({ count: 0, // ✅ реактивно user: { name: 'Иван', // ❌ НЕ реактивно age: 25, // ❌ НЕ реактивно },});
state.count++; // ✅ обновлениеstate.user.name = 'XX'; // ❌ нет обновленияstate.user = { name: 'Пётр', age: 30 }; // ✅ обновление (первый уровень)Когда использовать:
- Большие объекты с неизменяемыми вложенными данными
- Иммутабельные структуры (заменяем целиком)
- Данные из внешних источников (API, Canvas, Three.js)
🔋 KeepAlive — кэширование компонентов
Заголовок раздела «🔋 KeepAlive — кэширование компонентов»<KeepAlive> сохраняет компонент в памяти при скрытии, вместо того чтобы уничтожать и создавать заново.
<template> <TabBar v-model:active="activeTab" />
<!-- Без KeepAlive: каждое переключение вкладки — пересоздание компонента --> <component :is="activeTabComponent" />
<!-- С KeepAlive: компонент "засыпает" и "просыпается" --> <KeepAlive> <component :is="activeTabComponent" /> </KeepAlive></template><!-- Управление кэшированием --><KeepAlive :include="['ProfileTab', 'SettingsTab']" :exclude="['TempComponent']" :max="5"> <!-- max: максимум 5 компонентов в кэше --> <component :is="currentComponent" /></KeepAlive>// Хуки для компонентов внутри KeepAliveexport default defineComponent({ setup() { // Вызывается когда компонент "просыпается" (стал видимым) onActivated(() => { console.log('Компонент активирован — обновить данные'); fetchLatestData(); });
// Вызывается когда компонент "засыпает" (скрывается) onDeactivated(() => { console.log('Компонент деактивирован — остановить таймеры'); clearInterval(timer); }); },});🦥 Ленивые компоненты
Заголовок раздела «🦥 Ленивые компоненты»Разбивай большое приложение на чанки — загружай компоненты только когда они нужны.
// defineAsyncComponent — ленивая загрузка компонентаimport { defineAsyncComponent } from 'vue';
// Простой вариантconst HeavyChart = defineAsyncComponent(() => import('./components/HeavyChart.vue'));
// Расширенный вариант с обработкой состоянийconst AdminPanel = defineAsyncComponent({ loader: () => import('./components/AdminPanel.vue'), loadingComponent: LoadingSpinner, // Пока загружается errorComponent: ErrorDisplay, // Если ошибка delay: 200, // Задержка показа loadingComponent timeout: 10000, // Таймаут (мс) onError(error, retry, fail, attempts) { if (attempts <= 3) retry(); // Повторить до 3 раз else fail(); },});<!-- Используем с Suspense --><template> <Suspense> <!-- Основной контент --> <template #default> <HeavyChart :data="chartData" /> </template>
<!-- Fallback пока грузится --> <template #fallback> <div class="skeleton-chart" /> </template> </Suspense></template>// Роутер тоже поддерживает ленивую загрузкуconst routes = [ { path: '/dashboard', // Этот компонент загрузится только при переходе на /dashboard component: () => import('./views/DashboardView.vue'), }, { path: '/admin', // Группируем в один чанк с webpackChunkName component: () => import(/* webpackChunkName: "admin" */ './views/AdminView.vue'), },];📜 Виртуальный скролл
Заголовок раздела «📜 Виртуальный скролл»Если у тебя список из 10 000 элементов — нельзя рендерить все сразу. Виртуальный скролл рендерит только видимые элементы.
npm install vue-virtual-scroller<template> <!-- Рендерит только ~20 видимых строк из 100 000 --> <RecycleScroller class="scroller" :items="hugeList" :item-size="48" key-field="id" v-slot="{ item }" > <div class="user-item"> <img :src="item.avatar" /> <span>{{ item.name }}</span> <span>{{ item.email }}</span> </div> </RecycleScroller></template>
<style scoped>.scroller { height: 500px; /* ОБЯЗАТЕЛЬНО задать высоту */}.user-item { height: 48px; /* ОБЯЗАТЕЛЬНО соответствует item-size */ display: flex; align-items: center; gap: 0.5rem;}</style>
<script setup lang="ts">import { RecycleScroller } from 'vue-virtual-scroller';import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
// 100 000 элементов — без проблем!const hugeList = Array.from({ length: 100_000 }, (_, i) => ({ id: i, name: \`Пользователь \${i}\`, email: \`user\${i}@test.com\`, avatar: \`https://i.pravatar.cc/48?img=\${i % 70}\`,}));</script>📦 Оптимизация бандла
Заголовок раздела «📦 Оптимизация бандла»// vite.config.ts — разделение чанковimport { defineConfig } from 'vite';import vue from '@vitejs/plugin-vue';
export default defineConfig({ plugins: [vue()], build: { rollupOptions: { output: { // Разделяем вендоры от кода приложения manualChunks: { 'vendor-vue': ['vue', 'vue-router', 'pinia'], 'vendor-ui': ['@headlessui/vue', '@heroicons/vue'], 'vendor-charts': ['chart.js', 'vue-chartjs'], }, }, }, // Анализ размера бандла reportCompressedSize: true, },});npm install -D rollup-plugin-visualizer
# Добавить в vite.config.tsimport { visualizer } from 'rollup-plugin-visualizer';plugins: [vue(), visualizer({ open: true })]// Трясём дерево (Tree-shaking) — импортируй только нужное// ❌ Плохо — импортируем всю библиотекуimport _ from 'lodash';_.debounce(fn, 300);
// ✅ Хорошо — импортируем одну функциюimport debounce from 'lodash/debounce';debounce(fn, 300);
// ✅ Ещё лучше — lodash-es с tree-shakingimport { debounce } from 'lodash-es';debounce(fn, 300);🖼️ Оптимизация изображений
Заголовок раздела «🖼️ Оптимизация изображений»<template> <!-- Нативная ленивая загрузка --> <img loading="lazy" :src="image.url" :alt="image.alt" width="400" height="300" />
<!-- С плейсхолдером через IntersectionObserver --> <img ref="imgRef" :src="isVisible ? image.url : placeholder" :alt="image.alt" /></template>
<script setup lang="ts">const imgRef = ref<HTMLImageElement | null>(null);const isVisible = ref(false);
const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { isVisible.value = true; observer.disconnect(); }});
onMounted(() => { if (imgRef.value) observer.observe(imgRef.value);});onUnmounted(() => observer.disconnect());</script>💡 Чеклист производительности Vue 3
Заголовок раздела «💡 Чеклист производительности Vue 3»✅ Используй v-once для статичного контента✅ Используй v-memo для больших списков✅ shallowRef/shallowReactive для больших объектов✅ KeepAlive для часто переключаемых компонентов✅ defineAsyncComponent для тяжёлых компонентов✅ Виртуальный скролл для списков 100+ элементов✅ Разделяй бандл на чанки✅ Tree-shaking — импортируй точечно✅ loading="lazy" для изображений✅ Измеряй ПЕРЕД оптимизацией!📊 Итог: инструменты производительности
Заголовок раздела «📊 Итог: инструменты производительности»| Инструмент | Когда использовать |
|---|---|
v-once | Контент никогда не меняется |
v-memo | Большие списки с частичными обновлениями |
shallowRef | Большие объекты, заменяемые целиком |
KeepAlive | Вкладки, фильтры, переключаемые панели |
defineAsyncComponent | Тяжёлые компоненты, редко используемые |
| Virtual Scroll | Списки 100+ элементов |
| Code Splitting | Большой бандл, много роутов |
Vue 3 даёт тебе все инструменты для создания молниеносных приложений. Главное правило — сначала измеряй, потом оптимизируй. Преждевременная оптимизация — корень всех зол. 🎯