Svelte — компилируемый фреймворк

Svelte превращает компоненты в чистый JavaScript на этапе сборки, поэтому в браузер не попадает тяжёлый рантайм. В этом уроке ты освоишь реактивность, стора и SvelteKit — мета-фреймворк для страниц, маршрутов и деплоя.

Уровень: начальный 17 разделов 2–3 часа

1. Что такое Svelte и в чём его идея

Svelte отличается от React и Vue принципиально: он не работает в браузере с виртуальным DOM. Компилятор на этапе сборки превращает компоненты в обычный JavaScript, который напрямую обновляет DOM. Итог — маленький итоговый файл без тяжёлого рантайма.

// Компонент Svelte
<script>
  let count = 0
</script>

<button on:click={() => count += 1}>
  Кликов: {count}
</button>
Простая мысль для старта: компонент Svelte — это один файл, где <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 также полноценно поддерживается TypeScript. При создании проекта через Vite достаточно выбрать шаблон 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
Сторы — отдельные модули, поэтому они работают и вне компонентов: в таймерах, WebSocket-подключениях и обычных функциях.

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 dev
src/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-nodeNode.js-сервер
adapter-vercelVercel (serverless)
adapter-netlifyNetlify (serverless)
adapter-cloudflareCloudflare Pages
Выбирай адаптер до деплоя: для статики SvelteKit предупредит, если страница пытается использовать серверные функции, а serverless-адаптеры требуют API-маршрутов.

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.
Транзишены срабатывают только при монтировании и разрушении элемента. Если блок меняет контент, но остаётся на месте — используй keyed each: {#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 — что оно вычисляется из других значений.

Runes работают и в обычных .svelte.js-модулях: общее состояние выносится из компонента без стора.
Стор writable никуда не делся — но для локального состояния внутри компонента runes проще и читаются нативно.
Не смешивай два стиля в одном файле: компилятор включает 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-тесты без разметки.
Не тестируй анимации и переходы: транзишены в jsdom не запускаются, тест будет «падать» по таймингу. Покрывай поведение, а не косметику.

16. Подводные камни Svelte

Реактивность Svelte обманчиво проста: кажется, что всё обновляется само. Вот места, где новичков ждут ошибки.

// Правильно: переназначение массива
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 гарантирует очистку. Это универсальный паттерн для карт, графиков и слайдеров.

Декоративные библиотеки подключай лениво: динамический import внутри 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 }
  }
}
<form method="POST" action="?/login" use:enhance>
  <input name="email" />
  <button>Войти</button>
</form>

use:enhance добавляет асинхронность и обновление результата без перезагрузки страницы.

Один action — одна задача: разделите форму входа и форму регистрации на разные экшены, чтобы код не раздувался.
Не держите секреты в +page.svelte: всё, что касается аутентификации, — только в серверных модулях.

19. Интернационализация: многоязычное приложение

Многоязычный сайт поднимает вопросы не только перевода, но и URL, поисковика и валидации форм. Часто используется связка SvelteKit + библиотека переводов (svelte-i18n, paraglide) со словарями на каждый язык.

// messages/ru.json
{
  "greeting": "Привет, {name}!",
  "items": "{count} товаров"
}

Важно не переводить «лениво»: даты, склонения, валидация форм, атрибуты aria-label — всё это часть локализации, которую легко забыть.

Начните с одного-двух языков и структуры словарей: перенос на новый язык станет добавлением файла, а не перепиской компонентов.
Не смешивайте «перевод в коде» и «перевод текстов в DOM»: строки должны жить в едином словаре, иначе локализация разъедается.
Для видимости в поиске добавьте hreflang и избегайте редиректов, которые «ловят» пользователя в неверную локаль через куки.

20. Загрузка данных и кэширование

В SvelteKit данные для страницы собирает серверная функция load — она запускается до рендера и всегда получает свежие значения. Это убирает «загрузку» на клиенте и упрощает кэширование между пользователями.

// +page.server.js
import { cache } from "../lib/cache";

export const load = async ({ params }) => ({
  product: await cache.fetch(`key:${params.slug}`),
});

Обновление на клиенте: скачанный результат остаётся в $page.data, и ревалидация после действия (например, добавления в корзину) перезапрашивает страницу точечно.

Выделите ключ кэша по параметрам запроса: «товар + язык» — два разных ключа, и не смешивайте локали.
Данные в load — потенциальные утечки: возвращайте только нужные поля пользователю, а не весь объект из базы.
Любые быстрые страницы — это и UX, и сео: серверный рендер load-данных окупается в обоих направлениях.

21. Ленивая загрузка и виртуализация списков

Очень длинные списки (лента, логи, каталог) и тяжёлые компоненты на странице — классический источник лагов. Решение в два приёма: грузить модули только когда понадобились, и рендерить лишь видимые строки.

// Ленивый динамический импорт
const Chart = () => import("./Chart.svelte");

{#if showChart}
  <Chart />
{/if}

Динамический import откладывает загрузку кода тяжёлого модуля до первого использования — начальный бандл становится меньше.

Мерьте, прежде чем оптимизировать: DevTools покажут, что тормозит — сеть, рендер или данные. Часто быстрее исправить запрос, а не вводить виртуализацию.
Виртуализация теряет скролл-анимации и фокус: для коротких списков (сотня строк) она скорее вредит, чем помогает.
Дешёвый ленивый импорт внутри циклов — антипаттерн: он порождает запросы на каждый элемент. Ленивьте сам виджет целиком.

Что дальше

Теперь ты умеешь строить реактивные компоненты, обмениваться состоянием через стора и собирать сайт на SvelteKit. Закрепи навык, написав список задач с добавлением и удалением пунктов. Загляни в курсы по Vite и Vitest — они отлично дополняют сборку и тесты Svelte-приложений.

← Вернуться к курсам