SSR и гидратация
Сервер не знает размера браузера, поэтому страница, которая рендерится по состоянию вьюпорта, рендерится по догадке, а потом на клиенте встречается с настоящим значением. Эта страница показывает три уровня борьбы с этим — от самого простого до того, что убирает несовпадение для вернувшихся посетителей.
Три уровня
| Уровень | Что рендерит сервер | Предупреждение при гидратации, если браузер отличается |
|---|---|---|
ssrState | Фиксированную раскладку, которую вы выбрали | Да, если гидратация не отложена |
| Отложенная гидратация | Ту же фиксированную раскладку | Нет — настоящее значение приходит после монтирования |
| Подсказки запроса | Раскладку, выбранную по запросу | Нет, и обычно нет скачка раскладки |
ssrState: фиксированная догадка
Без подсказки сервер рендерит каждый ключ как false, что обычно не соответствует никакой раскладке. Опция ssrState задаёт значения, которые ключ принимает вместо false, когда matchMedia (вьюпорт) или ResizeObserver (контейнер) недоступны:
const state = createResponsiveState(config, {
ssrState: { desktop: true },
})
state.getState() // на сервере: { mobile: false, tablet: false, desktop: true }
state.getSsrState() // те же значения — и на сервере, и в браузере- Ключи, которых нет в конфиге, игнорируются, а не перечисленные остаются
false. - В браузере опция игнорируется, и побеждает настоящее значение.
getSsrState()при этом по-прежнему возвращает засеянные значения — именно их адаптеры используют для безопасной гидратации. - Выбирайте раскладку, которую получает большинство посетителей, обычно
desktop.
Отложенная гидратация
Если настоящий размер браузера отличается от ssrState, первый рендер на клиенте отличается от серверного HTML, и Vue сообщает о несовпадении при гидратации. Отложенная гидратация это убирает: пока страница гидратируется, клиент использует те же значения, что и сервер, а настоящие подставляет сразу после монтирования.
Vue
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 и user agent
Фиксированная догадка неверна для каждого посетителя не на предполагаемом устройстве, и отложенная гидратация тогда на один кадр показывает не ту раскладку. Подсказки запроса дают серверу лучшую догадку из самого запроса:
cookie— клиент записывает собственный размер вьюпорта в cookie (<ширина>x<высота>, например390x844), а сервер читает её при следующем запросе. Для вернувшегося посетителя догадка точная.user-agent— сервер относит устройство к классуmobile,tabletилиdesktopи рендерит для типичного размера этого класса.
Подсказки применяются в заданном порядке; побеждает первая, давшая значение, а ssrState — запасной вариант.
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. Значения по умолчанию:
| Устройство | Ширина | Высота |
|---|---|---|
mobile | 390 | 844 |
tablet | 820 | 1180 |
desktop | 1440 | 900 |
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.