28. Производительность
⚡ Производительность Svelte: Почему это так быстро
Заголовок раздела «⚡ Производительность Svelte: Почему это так быстро»Привет! 👋 Svelte часто называют “самым быстрым фреймворком” — и это не просто маркетинг. Разберём почему Svelte быстр, как сделать его ещё быстрее, и как правильно измерять производительность.
Представь двух поваров 🍳. Первый (React) готовит через посредника (виртуальный DOM): сначала рисует блюдо на бумаге, сравнивает с предыдущим рисунком, и только потом идёт на кухню. Второй (Svelte) идёт прямо на кухню и меняет только то, что нужно. Кто быстрее?
🧠 Почему нет Virtual DOM — это хорошо
Заголовок раздела «🧠 Почему нет Virtual DOM — это хорошо»Virtual DOM — это концепция React/Vue, при которой каждое обновление проходит через:
1. Создать новый Virtual DOM (дерево объектов в памяти)2. Сравнить с предыдущим Virtual DOM (diffing)3. Вычислить минимальные изменения (reconciliation)4. Применить изменения к реальному DOM// React — при каждом setState происходит вся цепочка выше:function Counter() { const [count, setCount] = useState(0); // Re-render всего компонента → VDOM diff → DOM update return <button onClick={() => setCount(c => c + 1)}>{count}</button>;}<!-- Svelte — компилятор генерирует прямые DOM операции: --><script> let count = $state(0);</script><!-- Компилируется в что-то вроде: --><!-- document.createTextNode(count) --><!-- при изменении: textNode.data = count --><button onclick={() => count++}>{count}</button>Что генерирует Svelte компилятор:
// Упрощённый вывод компилятора для <p>{count}</p>:function create_fragment(ctx) { let p, t;
return { c() { p = element("p"); t = text(ctx[0]); // count }, m(target, anchor) { insert(target, p, anchor); append(p, t); }, p(ctx, [dirty]) { // dirty — битовая маска того, что изменилось if (dirty & 1) set_data(t, ctx[0]); // Обновляем только текст! }, d(detaching) { if (detaching) detach(p); } };}📦 Размер бандла: сравнение
Заголовок раздела «📦 Размер бандла: сравнение»Фреймворк Runtime в бандле Hello World приложение────────────────────────────────────────────────────────────────Vanilla JS 0 KB ~1 KBSvelte 5 ~10-15 KB ~5-10 KBVue 3 ~22 KB (min+gzip) ~30-40 KBReact 18 ~47 KB (min+gzip) ~130 KB (с ReactDOM)Angular 17 ~70+ KB ~200+ KBПочему Svelte такой маленький? Потому что нет runtime — компилятор генерирует эффективный JavaScript напрямую. Каждое приложение использует только то, что ему нужно.
🎯 svelte:options immutable
Заголовок раздела «🎯 svelte:options immutable»immutable={true} говорит Svelte: “Мои props никогда не мутируются — новое значение всегда новый объект.” Это позволяет делать быстрые проверки по ссылке:
<!-- ❌ Без immutable — Svelte проверяет каждый prop глубоко --><script> let { user } = $props();</script>
<!-- ✅ С immutable — Svelte только сравнивает ссылки (===) --><svelte:options immutable={true} /><script> let { user } = $props(); // Теперь если user === prevUser → компонент НЕ перерисовывается // Только при user !== prevUser (новый объект)</script>
<div>{user.name}</div><!-- Практичный пример — список элементов: --><svelte:options immutable={true} /><script lang="ts"> let { item }: { item: { id: string; name: string; price: number } } = $props();</script><div>{item.name}: {item.price} ₽</div><!-- Родительский компонент --><script lang="ts"> let items = $state([ { id: '1', name: 'Ноутбук', price: 80000 }, { id: '2', name: 'Мышь', price: 2000 }, ]);
function updatePrice(id: string, price: number) { // НОВЫЙ объект для элемента — immutable правило соблюдено items = items.map(item => item.id === id ? { ...item, price } : item ); // Svelte перерисует только тот ListItem, у которого изменилась ссылка }</script>
{#each items as item (item.id)} <ListItem {item} />{/each}🔑 {#key}: принудительный ремонтинг
Заголовок раздела «🔑 {#key}: принудительный ремонтинг»{#key} — это способ сказать Svelte: “Когда это значение меняется, создай компонент заново (ремонтируй)”:
<script lang="ts"> let userId = $state('user-1');
// При смене userId компонент UserProfile будет уничтожен и создан заново // Это полезно когда у компонента есть внутреннее состояние, // которое нужно сбросить</script>
{#key userId} <UserProfile id={userId} />{/key}
<!-- Без #key: компонент обновится (lifecycle сохранится) --><!-- С #key: компонент уничтожится и создастся заново --><!-- Анимации при смене контента: --><script lang="ts"> import { fade } from 'svelte/transition'; let currentPage = $state(1);</script>
{#key currentPage} <div transition:fade={{ duration: 200 }}> <PageContent page={currentPage} /> </div>{/key}💤 Lazy loading компонентов
Заголовок раздела «💤 Lazy loading компонентов»<script lang="ts"> // Ленивая загрузка — компонент не включается в начальный бандл: let HeavyChart: any = null; let showChart = $state(false);
async function loadChart() { // Динамический импорт — создаёт отдельный чанк const module = await import('./HeavyChart.svelte'); HeavyChart = module.default; showChart = true; }</script>
<button onclick={loadChart}>Показать граф</button>
{#if showChart && HeavyChart} <svelte:component this={HeavyChart} data={chartData} />{/if}Code splitting в SvelteKit (автоматический):
// SvelteKit автоматически разбивает код по маршрутам// src/routes/heavy-page/+page.svelte → отдельный чанк
// Можно также lazy load внутри страницы:// src/routes/dashboard/+page.svelte<script lang="ts"> import { onMount } from 'svelte';
let DashboardChart: any = null;
onMount(async () => { // Загружаем только когда компонент смонтирован (клиент) const mod = await import('$lib/components/DashboardChart.svelte'); DashboardChart = mod.default; });</script>
{#if DashboardChart} <svelte:component this={DashboardChart} />{:else} <div class="skeleton">Загружаем граф...</div>{/if}🎨 svelte/motion: анимации без потерь производительности
Заголовок раздела «🎨 svelte/motion: анимации без потерь производительности»Svelte поставляется с двумя утилитами для плавных анимаций: tweened и spring. Они используют requestAnimationFrame и не влияют на реактивную систему.
<script lang="ts"> import { tweened, spring } from 'svelte/motion'; import { cubicOut, elasticOut } from 'svelte/easing';
// tweened — линейная анимация с easing const progress = tweened(0, { duration: 400, easing: cubicOut, });
// spring — физическая симуляция (пружина) const position = spring({ x: 0, y: 0 }, { stiffness: 0.1, // Жёсткость пружины (0-1) damping: 0.8, // Затухание (0-1) precision: 0.001, // Порог остановки });
let score = $state(0);
function updateScore(newScore: number) { score = newScore; progress.set(newScore / 100); // Анимируем прогресс-бар }
function handleMouseMove(event: MouseEvent) { position.set({ x: event.clientX, y: event.clientY }); // spring автоматически анимирует до нового значения! }</script>
<!-- Прогресс-бар с анимацией: --><div class="progress-container"> <div class="progress-bar" style="width: {$progress * 100}%"></div></div>
<!-- Следящий элемент: --><div class="follower" style="transform: translate({$position.x}px, {$position.y}px)" onmousemove={handleMouseMove}></div>
<!-- Счётчик с tweened: --><button onclick={() => updateScore(Math.floor(Math.random() * 100))}> Рандомный счёт</button>Разница между tweened и spring:
tweened: - Фиксированная длительность - Точный контроль через easing функции - Идеально: прогресс-бары, счётчики, transitions
spring: - Динамическая длительность (пока не "успокоится") - Физическая симуляция — естественное движение - Идеально: курсоры, drag&drop, card hover effects📊 Web Vitals в SvelteKit
Заголовок раздела «📊 Web Vitals в SvelteKit»// src/routes/+layout.ts — измерение Web Vitalsimport type { LayoutLoad } from './$types';
export const load: LayoutLoad = () => { // Запускаем измерение Web Vitals только на клиенте if (typeof window !== 'undefined') { import('web-vitals').then(({ onCLS, onFID, onFCP, onLCP, onTTFB }) => { onCLS(sendToAnalytics); onFID(sendToAnalytics); onFCP(sendToAnalytics); onLCP(sendToAnalytics); onTTFB(sendToAnalytics); }); } return {};};
function sendToAnalytics(metric: any) { // Отправляем в Vercel Analytics, Plausible, etc. console.log('Web Vital:', metric.name, metric.value);
if (typeof window !== 'undefined' && 'sendBeacon' in navigator) { navigator.sendBeacon('/api/analytics', JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, delta: metric.delta, })); }}Оптимизация LCP (Largest Contentful Paint):
<!-- src/routes/+layout.svelte — предзагрузка критических ресурсов --><svelte:head> <!-- Preload критического шрифта: --> <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
<!-- Preload LCP изображения: --> <link rel="preload" href="/hero-image.webp" as="image" type="image/webp" /></svelte:head>🔍 Bundle Analyzer
Заголовок раздела «🔍 Bundle Analyzer»npm install -D rollup-plugin-visualizerimport { sveltekit } from '@sveltejs/kit/vite';import { defineConfig } from 'vite';import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({ plugins: [ sveltekit(), visualizer({ open: true, // Автооткрытие в браузере filename: 'stats.html', gzipSize: true, // Показывать gzip размер brotliSize: true, // Показывать brotli размер template: 'treemap', // Тип диаграммы: treemap | sunburst | network }), ],});
// npm run build → откроется stats.html с визуализацией бандла🏎️ Оптимизация изображений
Заголовок раздела «🏎️ Оптимизация изображений»<script lang="ts"> // @sveltejs/enhanced-img — оптимизация изображений в SvelteKit import { Picture } from '@sveltejs/enhanced-img'; import heroImg from '$lib/assets/hero.jpg';</script>
<!-- Автоматически: - Конвертирует в WebP/AVIF - Генерирует srcset для разных размеров - Добавляет lazy loading - Предотвращает layout shift (width/height)--><enhanced:img src={heroImg} alt="Hero" />
<!-- Или с Picture для art direction: --><Picture src={heroImg} alt="Hero" sizes="(max-width: 768px) 100vw, 50vw"/>// vite.config.ts — настройка enhancedImages:import { enhancedImages } from '@sveltejs/enhanced-img';
export default defineConfig({ plugins: [ enhancedImages(), sveltekit(), ],});📈 Профилирование и измерение
Заголовок раздела «📈 Профилирование и измерение»<script lang="ts"> import { onMount, tick } from 'svelte';
let items = $state(Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` })));
// Измеряем время рендеринга: async function measure() { const start = performance.now();
items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Updated ${i}` }));
// tick() — ждём когда Svelte обновит DOM await tick();
const end = performance.now(); console.log(`Обновление 1000 элементов: ${(end - start).toFixed(2)}ms`); }
// Performance observer: onMount(() => { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`); } });
observer.observe({ entryTypes: ['measure', 'navigation', 'paint'] });
return () => observer.disconnect(); });</script>🧮 Чеклист оптимизации
Заголовок раздела «🧮 Чеклист оптимизации»Базовая оптимизация: ✅ Используй $derived вместо $effect для вычислений ✅ Используй {#each items as item (item.id)} с key ✅ immutable={true} для компонентов с неизменяемыми props ✅ Используй tweened/spring вместо CSS transitions для JS анимаций
Средняя оптимизация: ✅ Lazy load тяжёлых компонентов (графики, таблицы) ✅ {#key} для сброса состояния компонента ✅ Кэшируй дорогие вычисления в $derived ✅ Используй $state.raw() для неизменяемых данных
Продвинутая оптимизация: ✅ Bundle analyzer для анализа зависимостей ✅ Preload критических ресурсов ✅ Web Vitals мониторинг ✅ Оптимизированные изображения (@sveltejs/enhanced-img) ✅ Правильные HTTP кэш заголовки