1. Что такое Svelte
Что такое Svelte: компилятор, а не фреймворк 🚀
Заголовок раздела «Что такое Svelte: компилятор, а не фреймворк 🚀»Добро пожаловать в первый урок! Прежде чем писать хоть строчку кода, давай разберёмся с главным вопросом: а что вообще такое Svelte? Потому что ответ на него меняет всё. Когда я впервые познакомился со Svelte, у меня буквально сдвинулась парадигма — и я хочу, чтобы то же самое произошло с тобой 🤯
Компилятор vs фреймворк — аналогия переводчика 🔤
Заголовок раздела «Компилятор vs фреймворк — аналогия переводчика 🔤»Представь, что тебе нужно перевести книгу с французского на русский.
Способ 1 — Переводчик-синхронист: Ты едешь во Францию, берёшь с собой переводчика (50кг книг словарей), и он переводит всё на ходу, прямо во время чтения. Медленно, дорого, громоздко. Но переводчик всегда рядом — он обрабатывает любые изменения в тексте в реальном времени.
Способ 2 — Переводчик-редактор: Ты нанимаешь переводчика один раз, он переводит книгу заранее, и ты получаешь готовый текст. Переводчик тебе больше не нужен — книга уже переведена. Быстро, легко, портативно.
React и Vue — это переводчики-синхронисты. Они едут в браузер вместе с твоим кодом и работают там постоянно.
Svelte — это переводчик-редактор. Он переводит твой код заранее (во время сборки), а в браузер отправляется уже готовый ванильный JavaScript.
Обычный фреймворк:[Твой код] + [Фреймворк ~45kb] → Браузер → Виртуальный DOM → Реальный DOM
Svelte:[Твой .svelte] → Компилятор → [Ванильный JS ~1.7kb] → Браузер → Реальный DOMРазница принципиальная!
Виртуальный DOM — это оверхед 🐢
Заголовок раздела «Виртуальный DOM — это оверхед 🐢»Rich Harris написал знаменитую статью «Virtual DOM is pure overhead» (Виртуальный DOM — чистые накладные расходы). Вот суть идеи:
Что такое виртуальный DOM?
Это JavaScript-объект, который представляет собой «снимок» текущего состояния UI. При каждом изменении данных фреймворк:
- Создаёт новый виртуальный DOM
- Сравнивает его со старым (diffing/reconciliation)
- Вычисляет минимальный набор изменений
- Применяет изменения к реальному DOM
// Виртуальный DOM — это просто объект:const vdom = { type: 'div', props: { className: 'counter' }, children: [ { type: 'button', props: {}, children: ['−'] }, { type: 'span', props: {}, children: [42] }, { type: 'button', props: {}, children: ['+'] } ]}Звучит умно, но есть нюанс: весь этот процесс сравнения — накладные расходы. Каждое изменение данных запускает пересоздание и сравнение дерева объектов. Это работает достаточно быстро в большинстве случаев — но зачем платить эту цену, если можно не платить?
Что делает Svelte вместо этого?
Компилятор Svelte анализирует твой код и знает точно, какие DOM-узлы зависят от каких переменных. Поэтому он генерирует хирургически точные обновления:
// Svelte знает: 'count' привязан к этому текстовому узлу// Когда count меняется — обновляется ТОЛЬКО этот узелspan_text.data = count; // прямое DOM-обновление, никакого diffing!Никакого виртуального DOM. Никакого diffing. Просто точное, быстрое DOM-обновление.
История Svelte 📅
Заголовок раздела «История Svelte 📅»2016 — Рождение идеи
Заголовок раздела «2016 — Рождение идеи»Rich Harris, разработчик из The Guardian (британская газета), создал Svelte v1 и назвал его «магически исчезающим фреймворком». Идея была революционной: что если фреймворк вообще не нужен в рантайме?
Первый Svelte был интересным экспериментом, но синтаксис был неудобным, и он остался практически незамеченным.
2019 — Svelte 3: Перезапуск, который изменил всё ⭐
Заголовок раздела «2019 — Svelte 3: Перезапуск, который изменил всё ⭐»В 2019 году Rich Harris выступил на конференции с докладом «Rethinking Reactivity» и представил Svelte v3. Доклад набрал более миллиона просмотров на YouTube. Это было что-то особенное.
Svelte 3 ввёл принципиально новый синтаксис реактивности:
<script> // Просто let — и это реактивно! let count = 0;
// $: — реактивное объявление (пересчитывается при изменении count) $: doubled = count * 2;
// $: — реактивный блок (выполняется при изменении) $: if (count > 10) { alert('Много нажатий!'); }</script>
<button on:click={() => count++}> Нажато {count} раз</button><p>Двойное: {doubled}</p>Это было красиво! Простой, чистый, декларативный синтаксис.
2021 — SvelteKit
Заголовок раздела «2021 — SvelteKit»Появился SvelteKit — полностековый метафреймворк для Svelte. Теперь можно было создавать full-stack приложения так же легко, как в Next.js.
2022 — SvelteKit 1.0
Заголовок раздела «2022 — SvelteKit 1.0»Стабильный релиз SvelteKit. Svelte окончательно стал production-ready для серьёзных проектов.
2023 — Svelte 4
Заголовок раздела «2023 — Svelte 4»Инкрементальные улучшения: лучший DX, меньше размер, лучший TypeScript. Svelte 4 стал быстрее и легче.
2024 — Svelte 5: Революция Runes 🚀
Заголовок раздела «2024 — Svelte 5: Революция Runes 🚀»Полностью переработанная система реактивности. Вместо «магии компилятора» — явные, предсказуемые сигналы (Runes). Подробнее разберём дальше в этом уроке.
Как работает компиляция Svelte ⚙️
Заголовок раздела «Как работает компиляция Svelte ⚙️»Давай заглянем под капот! .svelte файл состоит из трёх секций:
<!-- 1. СКРИПТ: логика компонента --><script> let count = $state(0); let doubled = $derived(count * 2);
function reset() { count = 0; }</script>
<!-- 2. ШАБЛОН: HTML-разметка (прямо в корне, без обёртки!) --><div class="counter"> <button onclick={() => count--}>−</button> <span>{count}</span> <button onclick={() => count++}>+</button> <p>Двойное: {doubled}</p> <button onclick={reset}>Сбросить</button></div>
<!-- 3. СТИЛИ: изолированные CSS (scoped!) --><style> .counter { display: flex; gap: 8px; align-items: center; }
button { /* Этот стиль применяется ТОЛЬКО к кнопкам в этом компоненте! */ background: #ff3e00; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; }</style>Svelte компилятор берёт этот файл и генерирует примерно следующий JavaScript:
// Упрощённый псевдокод того, что генерирует Svelte:
import { create_element, text, listen, set_data } from 'svelte/internal';
export function create_fragment(ctx) { let button_minus, button_plus, span, t_count;
return { // Создание DOM-узлов c() { button_minus = create_element('button'); button_minus.textContent = '−'; span = create_element('span'); t_count = text(ctx.count); button_plus = create_element('button'); button_plus.textContent = '+'; }, // Монтирование в DOM m(target) { target.appendChild(button_minus); target.appendChild(span); span.appendChild(t_count); target.appendChild(button_plus); listen(button_plus, 'click', () => ctx.count++); }, // ТОЧЕЧНОЕ обновление — вызывается только когда нужно! p(ctx, dirty) { if (dirty & /*count*/ 1) { set_data(t_count, ctx.count); // обновляем только текст! } if (dirty & /*doubled*/ 2) { set_data(t_doubled, ctx.doubled); } } };}Ключевой момент — метод p() (patch). Svelte знает заранее, какие переменные влияют на какие DOM-узлы, поэтому обновляет только нужные. Никакого diffing!
Svelte vs React — ментальные модели 🧠
Заголовок раздела «Svelte vs React — ментальные модели 🧠»Посмотрим на один и тот же компонент в двух фреймворках:
React: данные → состояние → рендер → DOM
Заголовок раздела «React: данные → состояние → рендер → DOM»import { useState, useMemo } from 'react';
export default function Counter() { // Состояние через useState hook const [count, setCount] = useState(0);
// Вычисляемое значение — useMemo для оптимизации const doubled = useMemo(() => count * 2, [count]);
// При каждом изменении count — ВЕСЬ компонент рендерится заново return ( <div className="counter"> <button onClick={() => setCount(c => c - 1)}>−</button> <span>{count}</span> <button onClick={() => setCount(c => c + 1)}>+</button> <p>Двойное: {doubled}</p> </div> );}Svelte 5: данные → сигналы → точечный DOM update
Заголовок раздела «Svelte 5: данные → сигналы → точечный DOM update»<script lang="ts"> // Состояние через $state rune let count = $state(0);
// Вычисляемое значение — $derived (автоматически!) let doubled = $derived(count * 2);
// Никаких хуков! Переменные — реактивны по умолчанию</script>
<div class="counter"> <button onclick={() => count--}>−</button> <span>{count}</span> <button onclick={() => count++}>+</button> <p>Двойное: {doubled}</p></div>Что заметно сразу:
- Svelte-код короче и читабельнее
- Нет
import { useState }и прочих хуков let— и это уже реактивная переменная- Нет
setCount(c => c + 1)— простоcount++ - Нет
useMemo—$derivedсам знает, когда пересчитываться
React думает категориями «компонент рендерится заново». Svelte думает категориями «этот DOM-узел обновляется». Это принципиально разные ментальные модели.
Когда выбирать Svelte? 🎯
Заголовок раздела «Когда выбирать Svelte? 🎯»Svelte особенно хорош в следующих сценариях:
✅ Идеально для Svelte:├── 🌐 Контентные сайты и лендинги (маленький бандл!)├── ⚡ Производительность-критичные приложения├── 🎮 Браузерные игры (встроенные анимации и transitions)├── 📱 Progressive Web Apps (легкий рантайм)├── 🔌 Встраиваемые виджеты (изолированные компоненты)├── 📚 Образовательные проекты (простой синтаксис)└── 🚀 Стартапы (быстрая итерация, меньше бойлерплейта)
🤔 Подумай дважды:├── 📦 Огромная экосистема нужна прямо сейчас (у React больше)├── 👥 Команда глубоко знает React/Angular (порог входа)├── 🏢 Enterprise с жёсткими корпоративными требованиями└── 🔧 Специфические библиотеки только для ReactВ 2024 году Svelte занял 2-е место по удовлетворённости разработчиков в State of JS (уступив лишь Solid.js). Разработчики его любят. И есть за что!
Размер бандла 📦
Заголовок раздела «Размер бандла 📦»Числа говорят сами за себя:
Фреймворк Рантайм (gzip) Примечание─────────────────────────────────────────────Svelte ~1.7kb Только адаптерVue 3 ~34kb Core + компилятор шаблоновReact + DOM ~45kb react + react-domAngular ~103kb Минимальный наборНо важно понимать: для маленьких приложений Svelte может иметь больший бандл, чем React. Потому что в каждый компонент Svelte встраивает код обновления DOM.
Для тривиального <Counter>:├── React: ~45kb рантайм + ~200 байт компонент = ~45.2kb└── Svelte: ~1.7kb адаптер + ~600 байт компонент = ~2.3kb
Для среднего SPA с 50 компонентами:├── React: ~45kb + ~25kb компоненты = ~70kb└── Svelte: ~1.7kb + ~30kb компоненты = ~32kb ← Svelte выигрывает!Чем больше приложение — тем больше выигрывает Svelte. Потому что рантайм оплачивается один раз.
Svelte 5 Runes — революция ✨
Заголовок раздела «Svelte 5 Runes — революция ✨»В Svelte 5 появилась новая система реактивности, основанная на Runes (рунах). Это специальные функции-сигналы, которые компилятор распознаёт и превращает в реактивный код.
$state() — реактивное состояние
Заголовок раздела «$state() — реактивное состояние»<script> // Примитивы let count = $state(0); let name = $state('Яша'); let isOpen = $state(false);
// Объекты — глубоко реактивны! let user = $state({ name: 'Яша', age: 25, preferences: { theme: 'dark' } });
// Изменение вложенного свойства — реактивно! user.preferences.theme = 'light'; // ✅ обновит DOM</script>$derived() — вычисляемые значения
Заголовок раздела «$derived() — вычисляемые значения»<script> let items = $state([1, 2, 3, 4, 5]); let filter = $state('all');
// Автоматически пересчитывается при изменении items или filter let filtered = $derived( filter === 'even' ? items.filter(n => n % 2 === 0) : items );
// Для сложной логики — $derived.by() let stats = $derived.by(() => { const sum = items.reduce((a, b) => a + b, 0); return { sum, avg: sum / items.length, count: items.length }; });</script>$effect() — побочные эффекты
Заголовок раздела «$effect() — побочные эффекты»<script> let query = $state('');
// Запускается после каждого изменения query $effect(() => { // Автоматически отслеживает зависимости! console.log('Поиск:', query); const timer = setTimeout(() => search(query), 300);
// Функция очистки (как return в useEffect) return () => clearTimeout(timer); });</script>$props() — пропсы компонента
Заголовок раздела «$props() — пропсы компонента»<script> // Вместо export let в Svelte 4 let { name, age = 18, onUpdate } = $props(); // ^^^^ ^^^^^^^^ ^^^^^^^^ // | значение callback // | по умолчанию // обязательный prop</script>
<p>Привет, {name}! Тебе {age} лет.</p><button onclick={onUpdate}>Обновить</button>Реальный .svelte файл 📄
Заголовок раздела «Реальный .svelte файл 📄»Вот более реальный пример — компонент карточки пользователя:
<script lang="ts"> interface User { id: number; name: string; email: string; avatar?: string; }
let { user, onDelete }: { user: User; onDelete: (id: number) => void } = $props();
let isExpanded = $state(false); let initials = $derived( user.name.split(' ').map(n => n[0]).join('').toUpperCase() );</script>
<div class="card" class:expanded={isExpanded}> <div class="header" onclick={() => isExpanded = !isExpanded}> {#if user.avatar} <img src={user.avatar} alt={user.name} /> {:else} <div class="avatar-placeholder">{initials}</div> {/if}
<div class="info"> <h3>{user.name}</h3> <p>{user.email}</p> </div>
<span class="toggle">{isExpanded ? '▲' : '▼'}</span> </div>
{#if isExpanded} <div class="details" transition:slide> <p>ID: {user.id}</p> <button onclick={() => onDelete(user.id)} class="delete"> Удалить </button> </div> {/if}</div>
<style> .card { border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; transition: box-shadow 0.2s; }
.card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); }
.header { display: flex; align-items: center; gap: 12px; padding: 16px; cursor: pointer; }
.avatar-placeholder { width: 40px; height: 40px; border-radius: 50%; background: #ff3e00; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; }
/* Стили изолированы — не вытекают наружу! */ .delete { background: #ef4444; color: white; border: none; padding: 6px 14px; border-radius: 6px; cursor: pointer; }</style>Обрати внимание на несколько вещей:
- Шаблон — прямо в корне файла, без лишней обёртки
<template>(как во Vue) - CSS автоматически изолирован по умолчанию (scoped)
{#if},{:else},{/if}— встроенные логические блокиtransition:slide— встроенные анимации из коробки!
TypeScript в Svelte 🔷
Заголовок раздела «TypeScript в Svelte 🔷»Svelte поддерживает TypeScript нативно. Достаточно добавить lang="ts":
<script lang="ts"> // Всё работает с TypeScript! interface Todo { id: number; text: string; done: boolean; priority: 'low' | 'medium' | 'high'; }
let todos = $state<Todo[]>([]); let filter = $state<'all' | 'active' | 'done'>('all');
let filtered = $derived( filter === 'all' ? todos : filter === 'active' ? todos.filter(t => !t.done) : todos.filter(t => t.done) );
function addTodo(text: string): void { todos.push({ id: Date.now(), text, done: false, priority: 'medium' }); }
function toggleTodo(id: number): void { const todo = todos.find(t => t.id === id); if (todo) todo.done = !todo.done; }</script>
<!-- Шаблон -->{#each filtered as todo (todo.id)} <div class:done={todo.done}> <input type="checkbox" checked={todo.done} onchange={() => toggleTodo(todo.id)} /> <span>{todo.text}</span> </div>{/each}TypeScript в Svelte работает отлично с VSCode + расширением Svelte for VS Code. Все пропсы, store-значения и переменные — полностью типизированы.
Резюме ⚡
Заголовок раздела «Резюме ⚡»Свели всё воедино:
- Svelte — компилятор, а не runtime фреймворк. Он превращает
.svelteфайлы в ванильный JS во время сборки - Виртуальный DOM не нужен — Svelte генерирует точечные DOM-обновления, зная зависимости статически
- История: 2016 (v1) → 2019 (v3 🔥) → 2021 (SvelteKit) → 2024 (v5 Runes)
.svelteфайл состоит из<script>, шаблона и<style>— стили изолированы автоматически- Svelte 5 Runes:
$state(),$derived(),$effect(),$props()— явная, предсказуемая реактивность - Размер бандла: ~1.7kb рантайм vs ~45kb у React — Svelte выигрывает на средних и больших приложениях
- TypeScript работает из коробки с
<script lang="ts">
На следующем уроке разберём установку проекта с Vite и создадим первое Svelte-приложение! 🔥