Svelte

Состояние в SvelteKit: SSR-безопасно

Разработчик переносит привычку из чистого Svelte: заводит общий стор в модуле, кладёт туда текущего пользователя и импортирует где надо. В браузере это работает. На сервере SvelteKit та же строка превращается в трудноуловимый баг: модуль на сервере один на всех, и пользователь Алиса вдруг видит данные Боба, потому что их запросы поделили одну модульную переменную. Корень в том, что сервер обслуживает много запросов параллельно одним и тем же кодом, а клиент - один пользователь на вкладку. Состояние в SvelteKit требует понимания этой границы.

  • Авторизация: пользователь в модульной переменной утекает между запросами, Алиса видит сессию Боба
  • Корзина: общий серверный стор смешивает товары разных покупателей под нагрузкой
  • Текущий маршрут и параметры: читаются из page объекта $app/state без своих глобальных переменных
  • Тема и язык интерфейса: request-scoped состояние через контекст, своё для каждого SSR-запроса
  • Кэш конфигурации: неизменяемые данные в модуле безопасны, изменяемое пользовательское - нет

Предварительные знания

  • Руны $state и $derived и универсальная реактивность в файлах .svelte.js
  • Контекст компонентов Svelte: setContext и getContext
  • Понятие SSR: серверный код исполняется для каждого входящего запроса

Почему модульная переменная небезопасна на сервере

В чистом клиентском Svelte распространён приём: объявить реактивное состояние на верхнем уровне модуля и импортировать его в любой компонент. В браузере вкладка - это один пользователь, и общий модуль обслуживает только его. На сервере картина иная: один процесс Node обрабатывает запросы многих пользователей параллельно, и модуль загружается один раз на всех. Изменяемая переменная на уровне модуля становится общей для всех этих запросов.

Проблема проявляется только под параллельной нагрузкой, поэтому в разработке с одним пользователем её часто не замечают. В проде же два одновременных запроса делят одну переменную: запись одного перетирает данные другого. Это не редкая экзотика, а прямое следствие того, что серверный модуль - синглтон на процесс.

  • Браузер — Модуль живёт в одной вкладке одного пользователя. Общая переменная обслуживает только его, перемешивать нечего
  • Сервер — Один модуль на процесс обслуживает все параллельные запросы. Изменяемая общая переменная утекает между пользователями

Неизменяемые данные в модуле безопасны: константы, загруженная один раз конфигурация, чистые функции. Опасно именно изменяемое состояние, специфичное для пользователя или запроса. Правило простое: пользовательское и запрос-специфичное состояние не живёт в модульных переменных на сервере.

Почему хранение текущего пользователя в изменяемой переменной на уровне модуля опасно при SSR?

Чтение состояния страницы через `$app/state`

Часть состояния не нужно хранить самому - его уже ведёт SvelteKit. Текущий url, params маршрута, данные из load, результат form action доступны через объект page из модуля `$app/state`. Это безопасно при SSR: page привязан к текущему запросу на сервере и к текущей странице в браузере, без общих переменных.

Поля page реактивны через руны Svelte 5: при навигации page.url и page.params обновляются, и разметка перерисовывается сама. page.data собирает данные всех load по цепочке layout и page, поэтому пользователя, загруженного в layout, видно на любой вложенной странице без повторного запроса.

Модуль `$app/state` на рунах пришёл на смену прежнему `$app/stores`, где те же данные были стором и читались как `$page` с автоподпиской. В свежих проектах SvelteKit 2 используют `$app/state`; обращение к page.url теперь прямое, без знака стора в шаблоне.

Поле pageЧто содержит
page.urlТекущий URL со строкой запроса
page.paramsЗначения динамических сегментов маршрута
page.dataОбъединённые данные всех load (layout + page)
page.formРезультат последнего form action
page.statusHTTP-статус текущего ответа

Компоненту нужен текущий путь и параметр маршрута id, реактивно обновляемые при навигации. Откуда их взять?

Request-scoped состояние через контекст

Когда приложению нужно собственное общее состояние - тема, язык, корзина - и оно должно быть своим у каждого запроса, его строят через контекст компонентов. setContext в корневом +layout.svelte создаёт экземпляр состояния и кладёт его в дерево компонентов текущего рендера. getContext в любом потомке достаёт тот же экземпляр. Поскольку дерево компонентов создаётся заново на каждый SSR-запрос, состояние не делится между пользователями.

Разница с модульной переменной принципиальна. Модуль один на процесс - состояние общее. Контекст создаётся вызовом createTheme внутри рендера layout, а рендер у каждого запроса свой - значит, у каждого запроса собственный экземпляр состояния. Реактивность при этом сохраняется: внутри лежит обычный `$state`, и изменение theme.mode перерисовывает всех потребителей.

Ключ контекста удобно делать Symbol, а не строку: символ уникален и не столкнётся с чужим контекстом из библиотеки. Функции-обёртки createTheme и getTheme прячут ключ внутри модуля, и потребитель работает с понятным API, не зная деталей.

Нужно общее изменяемое состояние темы, своё для каждого SSR-запроса и доступное во всём дереве. Как его организовать?

Связь с другими темами

Этот урок соединяет реактивность Svelte с реальностью серверного рендеринга:

  • Hooks и locals — Серверная сторона request-scoped состояния: locals живёт один запрос и не делится между пользователями
  • Универсальная реактивность — Руны в .svelte.js - тот же инструмент; вопрос лишь в безопасном месте их размещения при SSR
  • Load-функции — Пользовательские данные приходят из load и locals, а не из разделяемой модульной переменной

Итог

  • Сервер обслуживает много запросов одним экземпляром модуля; изменяемая переменная на уровне модуля разделяется между всеми пользователями
  • Хранить пользовательские данные в модульной переменной на сервере опасно: они утекают между параллельными запросами
  • Объект page из $app/state даёт реактивный доступ к url, params, data и form текущей страницы без своих глобальных переменных
  • $app/state пришёл на смену стор-ориентированному $app/stores: те же данные, но через руны и без подписки на стор
  • Request-scoped состояние строят через контекст: setContext в корневом layout создаёт экземпляр на запрос, getContext читает его в потомках

Связанные уроки

  • sv-22-hooks — event.locals - серверный аналог идеи request-scoped состояния: данные, живущие ровно один запрос, без утечки между пользователями
  • sv-10-universal-reactivity — Реактивные руны в .svelte.js уже знакомы; здесь разбирается, где их размещение безопасно при SSR, а где нет
  • sv-19-load-functions — Серверное состояние пользователя берут из load и locals, а не из общей модульной переменной
Состояние в SvelteKit: SSR-безопасно

0

1

Войти