Перейти к содержимому

1. Что такое Svelte

Что такое Svelte: компилятор, а не фреймворк 🚀

Заголовок раздела «Что такое Svelte: компилятор, а не фреймворк 🚀»

Добро пожаловать в первый урок! Прежде чем писать хоть строчку кода, давай разберёмся с главным вопросом: а что вообще такое Svelte? Потому что ответ на него меняет всё. Когда я впервые познакомился со Svelte, у меня буквально сдвинулась парадигма — и я хочу, чтобы то же самое произошло с тобой 🤯


Компилятор vs фреймворк — аналогия переводчика 🔤

Заголовок раздела «Компилятор vs фреймворк — аналогия переводчика 🔤»

Представь, что тебе нужно перевести книгу с французского на русский.

Способ 1 — Переводчик-синхронист: Ты едешь во Францию, берёшь с собой переводчика (50кг книг словарей), и он переводит всё на ходу, прямо во время чтения. Медленно, дорого, громоздко. Но переводчик всегда рядом — он обрабатывает любые изменения в тексте в реальном времени.

Способ 2 — Переводчик-редактор: Ты нанимаешь переводчика один раз, он переводит книгу заранее, и ты получаешь готовый текст. Переводчик тебе больше не нужен — книга уже переведена. Быстро, легко, портативно.

React и Vue — это переводчики-синхронисты. Они едут в браузер вместе с твоим кодом и работают там постоянно.

Svelte — это переводчик-редактор. Он переводит твой код заранее (во время сборки), а в браузер отправляется уже готовый ванильный JavaScript.

Обычный фреймворк:
[Твой код] + [Фреймворк ~45kb] → Браузер → Виртуальный DOM → Реальный DOM
Svelte:
[Твой .svelte] → Компилятор → [Ванильный JS ~1.7kb] → Браузер → Реальный DOM

Разница принципиальная!


Rich Harris написал знаменитую статью «Virtual DOM is pure overhead» (Виртуальный DOM — чистые накладные расходы). Вот суть идеи:

Что такое виртуальный DOM?

Это JavaScript-объект, который представляет собой «снимок» текущего состояния UI. При каждом изменении данных фреймворк:

  1. Создаёт новый виртуальный DOM
  2. Сравнивает его со старым (diffing/reconciliation)
  3. Вычисляет минимальный набор изменений
  4. Применяет изменения к реальному 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-обновление.


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>

Это было красиво! Простой, чистый, декларативный синтаксис.

Появился SvelteKit — полностековый метафреймворк для Svelte. Теперь можно было создавать full-stack приложения так же легко, как в Next.js.

Стабильный релиз SvelteKit. Svelte окончательно стал production-ready для серьёзных проектов.

Инкрементальные улучшения: лучший DX, меньше размер, лучший TypeScript. Svelte 4 стал быстрее и легче.

Полностью переработанная система реактивности. Вместо «магии компилятора» — явные, предсказуемые сигналы (Runes). Подробнее разберём дальше в этом уроке.


Давай заглянем под капот! .svelte файл состоит из трёх секций:

Counter.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!


Посмотрим на один и тот же компонент в двух фреймворках:

Counter.tsx
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>
);
}
Counter.svelte
<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:
├── 🌐 Контентные сайты и лендинги (маленький бандл!)
├── ⚡ Производительность-критичные приложения
├── 🎮 Браузерные игры (встроенные анимации и 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-dom
Angular ~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 (рунах). Это специальные функции-сигналы, которые компилятор распознаёт и превращает в реактивный код.

<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>
<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>
<script>
let query = $state('');
// Запускается после каждого изменения query
$effect(() => {
// Автоматически отслеживает зависимости!
console.log('Поиск:', query);
const timer = setTimeout(() => search(query), 300);
// Функция очистки (как return в useEffect)
return () => clearTimeout(timer);
});
</script>
<script>
// Вместо export let в Svelte 4
let { name, age = 18, onUpdate } = $props();
// ^^^^ ^^^^^^^^ ^^^^^^^^
// | значение callback
// | по умолчанию
// обязательный prop
</script>
<p>Привет, {name}! Тебе {age} лет.</p>
<button onclick={onUpdate}>Обновить</button>

Вот более реальный пример — компонент карточки пользователя:

UserCard.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 — встроенные анимации из коробки!

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-приложение! 🔥