Svelte — компилируемый фреймворк
Svelte превращает компоненты в чистый JavaScript на этапе сборки, поэтому в браузер не попадает тяжёлый рантайм. В этом уроке ты освоишь реактивность, стора и SvelteKit — мета-фреймворк для страниц, маршрутов и деплоя.
1. Что такое Svelte и в чём его идея
Svelte отличается от React и Vue принципиально: он не работает в браузере с виртуальным DOM. Компилятор на этапе сборки превращает компоненты в обычный JavaScript, который напрямую обновляет DOM. Итог — маленький итоговый файл без тяжёлого рантайма.
// Компонент Svelte
<script>
let count = 0
</script>
<button on:click={() => count += 1}>
Кликов: {count}
</button><script>, <style> и разметка живут вместе. Стили, написанные в компоненте, не выходят за его пределы.2. Установка и первый компонент
Официальный способ начать — шаблон через Vite. SvelteKit запускается отдельной командой, но для первых экспериментов хватит обычного Svelte.
npm create vite@latest my-app -- --template svelte
cd my-app
npm install
npm run dev<!-- src/App.svelte -->
<script>
const user = 'Анна'
</script>
<h1>Привет, {user}!</h1>svelte-ts.3. Реактивность: переменные и $
Любая переменная в <script> автоматически реактивна: при её изменении Svelte обновляет те места в разметке, где она используется. Для вычисляемых значений есть «реактивное присваивание» с префиксом $:.
<script>
let price = 100
let quantity = 2
$: total = price * quantity
$: hasDiscount = total > 150
function increase() {
quantity += 1
}
</script>
<p>Сумма: {total} руб.</p>
{#if hasDiscount}
<p class="badge">Применена скидка!</p>
{/if}
<button on:click={increase}>Добавить ещё один</button>// Несколько инструкций в одном реактивном блоке
$: {
console.log('Цена изменилась')
console.log('Скидка:', total >= 200)
}arr.push() DOM не обновит — используйте спред: items = [...items, item].4. Пропсы и переиспользование компонентов
Пропсы объявляются ключевым словом export — это буквально экспортируемая переменная. Родитель передаёт значения как обычные атрибуты.
<!-- PostCard.svelte -->
<script>
export let title
export let published = false // значение по умолчанию
</script>
<article>
<h3>{title}</h3>
{#if published}
<p>Опубликовано ✓</p>
{/if}
</article><!-- App.svelte -->
<script>
import PostCard from './PostCard.svelte'
</script>
<PostCard title="Урок по Svelte" published={true} />export let count = 0 даёт типобезопасность в TS и избавляет от бесконечных проверок «а передали ли значение».5. События и двустороннее связать bind
Обработчики в Svelte пишутся как on:click, on:input и так далее. Для форм удобен bind: — он связывает переменную и поле ввода в обе стороны.
<script>
let name = ''
let agree = false
</script>
<input type="text" bind:value={name} placeholder="Ваше имя">
<label>
<input type="checkbox" bind:checked={agree}>
Согласен с правилами
</label>
<p>Здравствуйте, {name || 'гость'}!</p>// Своё событие из дочернего компонента
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
function save() {
dispatch('saved', { id: 42 })
}
</script>
<button on:click={save}>Сохранить</button>bind:value для select, bind:group для радио-кнопок и bind:this для работы с самим DOM-элементом.6. Условная отрисовка и циклы
Логика в шаблоне записывается блоками: {#if}, {#each}, {#await}. Их закрывают парными {/if}, {/each} и {/await}.
<script>
let status = 'loading'
let items = [
{ id: 1, name: 'Молоко' },
{ id: 2, name: 'Хлеб' }
]
</script>
{#if items.length === 0}
<p>Корзина пуста</p>
{:else}
<ul>
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
</ul>
{/if}// Асинхронный блок для промисов
{#await promise}
<p>Загружаем данные...</p>
{:then data}
<p>Готово: {data.title}</p>
{:catch err}
<p>Ошибка: {err.message}</p>
{/await}#each указывай ключ (item.id), чтобы Svelte переиспользовал DOM при изменении списка. Это делает анимации и обновления быстрыми.7. Сторы: общее состояние
Стор хранит состояние, доступное любому компоненту. Простейший вариант — writable: подписка через префикс $ делает значение реактивным прямо в разметке.
// stores/cart.js
import { writable } from 'svelte/store'
export const cart = writable([])
export function addToCart(item) {
cart.update((items) => [...items, item])
}<!-- любой компонент -->
<script>
import { cart, addToCart } from './stores/cart'
</script>
<p>Товаров в корзине: {$cart.length}</p>
<button on:click={() => addToCart({ id: 1 })}>Добавить</button>| Стор | Назначение |
|---|---|
| writable | изменяемое значение |
| readable | только для чтения |
| derived | вычисляется из других сторов |
| custom store | своя логика и приватное API |
8. Доступность и жизненный цикл
Svelte встроенно следит за доступностью: невалидные <a>, отсутствие alt и другие ошибки подсвечиваются прямо в компиляторе. Жизненный цикл компонента обслуживают onMount и onDestroy.
<script>
import { onMount, onDestroy } from 'svelte'
let now = new Date()
const timer = setInterval(() => {
now = new Date()
}, 1000)
onMount(() => console.log('Компонент смонтирован'))
onDestroy(() => clearInterval(timer))
</script>
<time>{now.toLocaleTimeString()}</time>onDestroy обязательна для таймеров, подписок и слушателей — иначе ресурсы «утекут», а логика продолжит работать на фоне.9. SvelteKit: страницы и маршруты
SvelteKit — мета-фреймворк для целого сайта: файловый роутинг, SSR, адаптеры под любой хостинг. Страницы лежат в src/routes/, каждый файл +page.svelte — это маршрут.
npm create svelte@latest my-site
cd my-site
npm install
npm run devsrc/routes/
├── +layout.svelte // общий каркас
├── +page.svelte // страница /
├── about/
│ └── +page.svelte // страница /about
└── blog/
└── [slug]/
└── +page.svelte // страница /blog/значение// Данные страницы загружаются в +page.js / +page.server.js
export async function load({ fetch }) {
const res = await fetch('/api/posts')
return { posts: await res.json() }
}+layout.svelte оборачивает все вложенные маршруты: ступень шапки и подвала прописывается один раз.10. Адаптеры и деплой
Адаптер определяет, под какой хостинг собрать проект. Статический сайт — @sveltejs/adapter-static, Node-сервер — adapter-node, serverless — adapter-vercel или adapter-netlify.
// svelte.config.js
import adapter from '@sveltejs/adapter-static'
export default {
kit: {
adapter: adapter()
}
}npm run build
# собранный статический сайт в папке build/
npm run preview# Деплой через CLI адаптера
npx vercel deploy --prod| Адаптер | Куда собирает |
|---|---|
| adapter-static | статический сайт (Netlify, GitHub Pages) |
| adapter-node | Node.js-сервер |
| adapter-vercel | Vercel (serverless) |
| adapter-netlify | Netlify (serverless) |
| adapter-cloudflare | Cloudflare Pages |
11. Транзишены и анимации
Svelte компилирует CSS-переходы прямо в компонент, поэтому анимации обходятся без внешних библиотек. Директивы transition, in:, out: и animate: импортируются из модуля svelte/transition.
<script>
import { fade, fly } from 'svelte/transition'
export let items = []
</script>
{#each items as item}
<div transition:fade={{ duration: 200 }}>
<p transition:fly={{ y: 16 }}>{item}</p>
</div>
{/each}Когда элемент появляется и исчезает, Svelte сам создаёт и уничтожает анимацию без ручного управления классами. Параметры передаются объектом: длительность, задержка и специфичные для эффекта настройки.
animate:flip из svelte/animate: элементы плавно «переедут» на новые места без единой строчки JS.{#each items as item (item.id)}.12. Слоты и композиция компонентов
Слоты позволяют наполнять компонент содержимым снаружи. Простейший пример — карточка, принимающая произвольную разметку.
<!-- Card.svelte -->
<article class="card">
<slot />
</article><!-- Использование -->
<Card>
<h3>Заголовок</h3>
<p>Текст внутри карточки</p>
</Card>Именованные слоты (<slot name="footer">) дают несколько зон вставки, а слот-фолбэк в <slot>Запасной текст</slot> показывается, когда контент не передан.
Родители могут передавать несколько слотов сразу, а проброс свойств внутрь делается через слот-пропсы (let:props) — популярный паттерн для таблиц и списков. Если контент не пришёл, а подсказка нужна — фолбэк покажет её без единой правки на стороне родителя.
Слоты удобно сочетать с компонентами: <Modal> задаёт каркас и позиционирование, а контент и кнопки приходят снаружи. Такой компонент переиспользуется в десятках мест проекта, а изменения каркаса применяются сразу везде.
13. Работа с формами и валидация
Формы на Svelte строятся из обычного <form> с bind:value на полях. Отправка обрабатывается через on:submit с preventDefault.
<script>
let email = ''
let error = ''
function handleSubmit() {
if (!email.includes('@')) {
error = 'Введите корректный email'
return
}
error = ''
// отправка данных
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<input bind:value={email} placeholder="Email">
{#if error}
<p class="badge">{error}</p>
{/if}
<button type="submit">Отправить</button>
</form>Валидацию удобно выносить в отдельную функцию или в SvelteKit формировать в файле +page.server.js — тогда ошибки вернутся в form на страницу. Это типичная практика для продакшн-форм.
Пользователям помогают подсказки: подсвечивайте поле с ошибкой, показывайте текст рядом с полем, а не сверху страницы, и очищайте сообщение после правки. В SvelteKit проще всего это сделать через form и проверку поля в компоненте.
14. Svelte 5: runes вместо реактивных переменных
Svelte 5 меняет модель реактивности: на смену «сахарной» переменной пришли явные runes — $state, $derived, $props и $effect. Код становится предсказуемее, особенно внутри модулей за пределами компонента.
<script>
let count = $state(0)
let doubled = $derived(count * 2)
</script>
<button onclick={() => count += 1}>
Кликов: {count}, удвоено {doubled}
</button>Старые реактивные присваивания ($:) всё ещё работают, но новые проекты стоит писать на runes. Префикс $state явно говорит, что значение реактивно, а $derived — что оно вычисляется из других значений.
.svelte.js-модулях: общее состояние выносится из компонента без стора.writable никуда не делся — но для локального состояния внутри компонента runes проще и читаются нативно.15. Тестирование компонентов
Svelte легко тестировать: компоненты компилируются в обычные функции. Стандартная связка — Vitest с jsdom и Testing Library.
npm install -D vitest @testing-library/svelte jsdom
// counter.test.js
import { render, fireEvent } from '@testing-library/svelte'
import Counter from './Counter.svelte'
test('счётчик увеличивается', async () => {
const { getByRole, getByText } = render(Counter)
const btn = getByRole('button')
await fireEvent.click(btn)
expect(getByText('Кликов: 1')).toBeTruthy()
})Тестируй логику (функции модулей, стора) чаще, чем DOM: она стабильнее и быстрее. Для компонентов хватает нескольких смоук-тестов на ключевые сценарии.
vitest с environment: 'jsdom' и добавь скрипт в package.json. Стори и чистую логику выноси в отдельные unit-тесты без разметки.16. Подводные камни Svelte
Реактивность Svelte обманчиво проста: кажется, что всё обновляется само. Вот места, где новичков ждут ошибки.
- Мутация массива:
arr.push()не сработает — используйarr = [...arr, item]. - keyed each: без
(item.id)Svelte переиспользует DOM неверно, ломаются фокус и анимации. - Присвоение внутри блока: переменная, изменённая в
{#if}, может остаться старой после выхода из блока. - on:click vs onclick: в Svelte 5 события пишутся как обычные атрибуты
onclick.
// Правильно: переназначение массива
let items = []
function add(item) {
items = [...items, item] // новая ссылка — реактивность сработает
}Большая часть багов — из-за сохранения ссылки на один и тот же объект. Приём один: при изменении состояния всегда создавай новую ссылку.
onMount, проверь, не мутируешь ли ты объект «на месте» вместо присвоения новой ссылки.svelte-check и ESLint: типичные ловушки ловятся линтером до того, как ушли в продакшн.17. Интеграция с обычным JS и библиотеками
Svelte дружит с любым JS-кодом: компонент — это функция, а не изолированный мир. Для работы с DOM напрямую используй bind:this, а для подключения сторонних библиотек — жизненный цикл.
<script>
import { onMount, onDestroy } from 'svelte'
let chartEl
let chart
onMount(() => {
chart = new ChartLib(chartEl, { type: 'line' })
})
onDestroy(() => {
chart.destroy() // избегаем утечек
})
</script>
<div bind:this={chartEl}></div>bind:this даёт ссылку на DOM-элемент после монтирования, а onDestroy гарантирует очистку. Это универсальный паттерн для карт, графиков и слайдеров.
onMount уменьшит начальный бандл.bind:this, а не разбрасывай вызовы по всему приложению.18. Form actions: серверная обработка форм в SvelteKit
В SvelteKit формы могут обрабатываться на сервере без отдельного API: специальная функция в +page.server.js получает данные, валидирует их и возвращает результат странице. Это превращает форму в прогрессивное улучшение: без JS она тоже работает.
// +page.server.js
export const actions = {
default: async ({ request }) => {
const data = await request.formData()
const email = data.get("email")
if (!email) return fail(400, { error: "Введите email" })
return { success: true }
}
}- действия объявляются как
actionsи вызываются по имени (?/login); failкодирует ошибку с кодом статуса, а значение ошибки доступно на странице через$page.form;- код выполняется на сервере — данные формы не попадают в клиентский бандл.
<form method="POST" action="?/login" use:enhance>
<input name="email" />
<button>Войти</button>
</form>use:enhance добавляет асинхронность и обновление результата без перезагрузки страницы.
+page.svelte: всё, что касается аутентификации, — только в серверных модулях.19. Интернационализация: многоязычное приложение
Многоязычный сайт поднимает вопросы не только перевода, но и URL, поисковика и валидации форм. Часто используется связка SvelteKit + библиотека переводов (svelte-i18n, paraglide) со словарями на каждый язык.
// messages/ru.json
{
"greeting": "Привет, {name}!",
"items": "{count} товаров"
}- локаль хранится в URL (
/ru/... /en/...) или в cookie — каждый подход по-своему виден поисковикам; - плюрализация решается не «строками», а правилами множественности;
- форматы дат и чисел — отдельная область: «12.09.2026» против «Sep 12, 2026».
Важно не переводить «лениво»: даты, склонения, валидация форм, атрибуты aria-label — всё это часть локализации, которую легко забыть.
20. Загрузка данных и кэширование
В SvelteKit данные для страницы собирает серверная функция load — она запускается до рендера и всегда получает свежие значения. Это убирает «загрузку» на клиенте и упрощает кэширование между пользователями.
// +page.server.js
import { cache } from "../lib/cache";
export const load = async ({ params }) => ({
product: await cache.fetch(`key:${params.slug}`),
});- данные грузятся на сервере один раз, страница получает готовый HTML (SEO + скорость);
- повторные запросы бьются в общий кэш (Redis, памяти сервера) — база разгружается;
- инвалидация — через ключ после изменений, а не «обновляем всё».
Обновление на клиенте: скачанный результат остаётся в $page.data, и ревалидация после действия (например, добавления в корзину) перезапрашивает страницу точечно.
21. Ленивая загрузка и виртуализация списков
Очень длинные списки (лента, логи, каталог) и тяжёлые компоненты на странице — классический источник лагов. Решение в два приёма: грузить модули только когда понадобились, и рендерить лишь видимые строки.
// Ленивый динамический импорт
const Chart = () => import("./Chart.svelte");
{#if showChart}
<Chart />
{/if}Динамический import откладывает загрузку кода тяжёлого модуля до первого использования — начальный бандл становится меньше.
- виртуализация списка: рендерятся только строки в области видимости + запас;
- интерсекшн-обсервер подгружает данные при прокрутке к краю;
- пагинация «по кнопке» или «бесконечная» — по чувству продукта.
Что дальше
Теперь ты умеешь строить реактивные компоненты, обмениваться состоянием через стора и собирать сайт на SvelteKit. Закрепи навык, написав список задач с добавлением и удалением пунктов. Загляни в курсы по Vite и Vitest — они отлично дополняют сборку и тесты Svelte-приложений.