Skip to content

SSR и гидратация ​

Сервер не знает размера браузера, поэтому страница, которая рендерится по состоянию вьюпорта, рендерится по догадке, а потом на клиенте встречается с настоящим значением. Эта страница показывает три уровня борьбы с этим — от самого простого до того, что убирает несовпадение для вернувшихся посетителей.

Три уровня ​

УровеньЧто рендерит серверПредупреждение при гидратации, если браузер отличается
ssrStateФиксированную раскладку, которую вы выбралиДа, если гидратация не отложена
Отложенная гидратацияТу же фиксированную раскладкуНет — настоящее значение приходит после монтирования
Подсказки запросаРаскладку, выбранную по запросуНет, и обычно нет скачка раскладки

ssrState: фиксированная догадка ​

Без подсказки сервер рендерит каждый ключ как false, что обычно не соответствует никакой раскладке. Опция ssrState задаёт значения, которые ключ принимает вместо false, когда matchMedia (вьюпорт) или ResizeObserver (контейнер) недоступны:

ts
const state = createResponsiveState(config, {
  ssrState: { desktop: true },
})

state.getState() // на сервере: { mobile: false, tablet: false, desktop: true }
state.getSsrState() // те же значения — и на сервере, и в браузере
  • Ключи, которых нет в конфиге, игнорируются, а не перечисленные остаются false.
  • В браузере опция игнорируется, и побеждает настоящее значение. getSsrState() при этом по-прежнему возвращает засеянные значения — именно их адаптеры используют для безопасной гидратации.
  • Выбирайте раскладку, которую получает большинство посетителей, обычно desktop.

Отложенная гидратация ​

Если настоящий размер браузера отличается от ssrState, первый рендер на клиенте отличается от серверного HTML, и Vue сообщает о несовпадении при гидратации. Отложенная гидратация это убирает: пока страница гидратируется, клиент использует те же значения, что и сервер, а настоящие подставляет сразу после монтирования.

Vue ​

ts
import { defineResponsive } from 'responsive-media/vue'

export const { useResponsive, plugin } = defineResponsive(config, {
  ssrState: { desktop: true },
  hydration: 'deferred',
})
  • hydration: 'immediate' (по умолчанию) — настоящее состояние используется с первого рендера, как раньше. Несовпадение возможно.
  • hydration: 'deferred' — плагин замораживает состояние на серверном снимке, пока приложение монтируется поверх серверной разметки, и применяет настоящие значения на следующем тике. Страница рендерится дважды, и второй рендер — настоящий.
  • commit — когда приходят настоящие значения. 'auto' (по умолчанию) — на следующем тике после монтирования. 'manual' держит значения сервера, пока вы не вызовете commitHydration(); это правильный выбор для страницы с асинхронными компонентами или ленивой гидратацией: они гидратируются позже первого тика, и при 'auto' встретили бы настоящее состояние и разошлись. commitTimeout (по умолчанию 10000 мс) выполняет переключение сам, если commitHydration() так и не вызвали; 0 отключает таймаут.
  • Клиентские приложения не откладываются. Плагин откладывает только тогда, когда в контейнере уже есть серверная разметка, или когда вы передали hydrating: true в app.use(plugin, { hydrating }). Обычный createApp в пустой элемент использует настоящее состояние сразу.
  • Плагин принимает их и как опции установки: app.use(plugin, { ssrState, hydration: 'deferred', commit: 'manual' }). defineResponsive() возвращает commitHydration рядом с plugin.

React ​

Настраивать ничего не нужно. Пока React гидратирует, useSyncExternalStore запрашивает у хуков серверный снимок, а хуки возвращают getSsrState(). Первый рендер на клиенте совпадает с серверным HTML, а потом React перерисовывает страницу с настоящим значением без recoverable-ошибки.

Nuxt ​

Модуль по умолчанию использует hydration: 'deferred', передаёт плагину nuxtApp.isHydrating и применяет настоящие значения, когда Nuxt сообщает, что страница догидратировалась (app:suspense:resolve), — включая асинхронные компоненты, — так что приложению на Nuxt дополнительный код не нужен. См. Интеграция с Nuxt.

Фиксированная догадка неверна для каждого посетителя не на предполагаемом устройстве, и отложенная гидратация тогда на один кадр показывает не ту раскладку. Подсказки запроса дают серверу лучшую догадку из самого запроса:

  • cookie — клиент записывает собственный размер вьюпорта в cookie (<ширина>x<высота>, например 390x844), а сервер читает её при следующем запросе. Для вернувшегося посетителя догадка точная.
  • user-agent — сервер относит устройство к классу mobile, tablet или desktop и рендерит для типичного размера этого класса.

Подсказки применяются в заданном порядке; побеждает первая, давшая значение, а ssrState — запасной вариант.

ts
import { resolveSsrState } from 'responsive-media'

const ssrState = resolveSsrState(
  config,
  { cookie: request.cookies['responsive-viewport'], userAgent: request.headers['user-agent'] },
  { hints: ['cookie', 'user-agent'], fallback: { desktop: true } },
)

resolveSsrState(config, input, options?)

  • input — { cookie?: string | null; userAgent?: string | null }.
  • options.hints — ('cookie' | 'user-agent')[] · по умолчанию: ['cookie', 'user-agent'].
  • options.devices — Partial<Record<'mobile' | 'tablet' | 'desktop', { width: number; height: number }>>. Размеры, для которых рендерится user agent. Значения по умолчанию:
УстройствоШиринаВысота
mobile390844
tablet8201180
desktop1440900
  • options.fallback — Record<string, boolean>. Значения, которые используются, когда ни одна подсказка не подошла.

Результат вычисляется подстановкой размера в конфиг, поэтому понимаются условия width, height, orientation и aspect-ratio — те же, что понимает состояние контейнера.

Составные части ​

  • detectDevice(userAgent) — 'mobile' | 'tablet' | 'desktop' | null (null для пустого user agent). iPad, запросивший десктопную версию сайта, присылает user agent от Mac и определяется как desktop.
  • serializeViewportCookie(size) и parseViewportCookie(value) — формат <ширина>x<высота>; парсер возвращает null для всего остального.
  • statesFromSize(config, size) — булево состояние конфига для заданного { width, height }.
  • VIEWPORT_COOKIE — имя cookie по умолчанию, 'responsive-viewport'.

В Nuxt ​

Задайте ssrHints в опциях модуля, и модуль сделает всё перечисленное сам — см. Интеграция с Nuxt.

Кэширование ​

При включённых подсказках HTML зависит от заголовков Cookie и User-Agent. CDN или обратный прокси, кэширующие страницы, должны учитывать их в Vary (Vary: Cookie, User-Agent) или не кэшировать эти страницы, иначе один посетитель получит раскладку другого. У предрендеренных страниц запроса нет, поэтому они всегда используют запасной ssrState.

Чего сервер знать не может ​

У useMediaQuery() и состояния контейнера серверного значения нет: useMediaQuery() равен false, а состояние контейнера пусто (ложно для любого ключа), пока управление не возьмёт браузер. Часть страницы, которая от них зависит и не должна расходиться, стоит поместить в <ClientOnly> или оставить её раскладку на CSS.