Skip to content
PПромтбук
RUEN
04Дебаг

Аудит SSR-гидратации

React / Next.js SSR hydration: где сервер и клиент расходятся, как поймать (console warnings, content flash), как починить без `suppressHydrationWarning`-обёрток.

Hydration mismatch — самый коварный класс SSR-багов. Внешне «работает», но: первый клик игнорируется, картинка моргает, форма теряет state. И главное — это часто кладёт LCP и Lighthouse-score.

Стек: {{stack}} Симптом: {{symptom}}

1. Что такое hydration и что значит mismatch

SSR-рендер: сервер генерирует HTML с готовой разметкой. Клиент получает этот HTML → React «гидратирует» (привязывает event handlers, восстанавливает state). Если разметка сервера и первого рендера клиента отличаются — mismatch.

Симптомы:

  • В console: Hydration failed because the initial UI does not match what was rendered on the server
  • Text content did not match. Server: "X" Client: "Y"
  • Did not expect server HTML to contain a <div> in <p> (невалидный HTML)
  • Видимо: текст / кнопка / картинка мигает после загрузки
  • Клик в первые 100-300ms ничего не делает (handlers ещё не привязаны)
  • Полный re-render всего поддерева → CLS, потерянный input focus

2. Топ-10 причин mismatch

1. Date.now() / new Date() в render

SSR рендер → одно значение. Client рендер → другое (прошло несколько ms). Mismatch.

Fix: вычисли в useEffect после mount или передай как props с сервера.

2. Math.random() / crypto.randomUUID()

Каждый render — новое значение. Гарантированный mismatch.

Fix: useId() для React 18+ или один раз в useEffect.

3. localStorage / sessionStorage в render

На сервере недоступно — fallback. Client читает реальное значение → mismatch.

Fix: инициализировать с null или server-default, читать в useEffect, ставить state.

4. window.matchMedia() / window.innerWidth

На сервере window нет → conditional render. Client → нормальный path.

Fix: library типа use-media с SSR-safe defaults, или CSS-only через media queries.

5. Browser locale / timezone

toLocaleString() без явного locale — сервер использует Node default, клиент — пользователя.

Fix: toLocaleString('en-US', ...) явный + UTC если возможно.

6. Third-party scripts модифицируют DOM до hydration

Adblock убирает <div class="ad">, Grammarly добавляет атрибуты, browser-extension расширяет.

Fix: suppressHydrationWarning на root <html> или <body> (документированный паттерн).

7. Conditional render на typeof window !== 'undefined'

SSR: server-branch. Client first render: тоже server-branch (window undefined? нет, есть!). Mismatch.

Fix: useEffect(() => setMounted(true), []) + if (!mounted) return null. Или dynamic({ ssr: false }).

8. Невалидный HTML

<div> внутри <p> или <table> без <tbody> — браузер автоматически закрывает теги, и DOM-структура отличается от того что React ожидает.

Fix: валидируй HTML структуру; используй <span> или подходящие теги.

9. CSS-in-JS streaming

Server insert'ит <style> в head в одном порядке, client — в другом.

Fix: правильный CSS-in-JS setup (Emotion / styled-components с SSR config).

10. Динамический контент с разной сериализацией

Сервер сериализует Date как "2026-01-01T00:00:00.000Z", клиент парсит в new Date() и formats иначе.

Fix: ISO strings consistently; форматирование только в useEffect.

3. Детектирование

a) Enable strict mode

// app/layout.tsx или _app.tsx
<React.StrictMode>...</React.StrictMode>

В dev mode выдаёт двойной render → mismatch виден сразу.

b) Проход с DevTools

  1. Открой prod (НЕ dev — там React показывает многое в overlay; в prod warnings уходят в console)
  2. localhost?reactDevtools=true или используй React DevTools extension
  3. Hard reload каждой страницы
  4. Console: ищи Hydration, did not match, Text content
  5. Network: что грузится медленнее чем должно
  6. Elements: сравни первоначальный HTML (View Source) и текущий DOM

c) Whyhydrate

Иногда warning указывает не на компонент с проблемой, а на родителя. Используй react-scan или whyhydrate (если доступно) — точно покажет.

d) Disable JS test

DevTools → Settings (F1) → Debugger → Disable JavaScript → перезагрузка. То что видишь — это server-render. Что должно появиться после JS — client-render. Сравни эти две картинки.

4. Fix patterns

a) Mount-gate (универсальный)

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null; // или skeleton

Cons: client-only render → пустота на сервере → SEO теряется, LCP сдвигается.

b) Initial state from server

function Component({ serverTimestamp }: { serverTimestamp: string }) {
  const [time, setTime] = useState(serverTimestamp); // matches server
  useEffect(() => {
    setTime(new Date().toISOString()); // update client-side
  }, []);
  return <span>{time}</span>;
}

Лучше: гидратация чистая, ничего не флэшит.

c) dynamic({ ssr: false })

const ClientOnly = dynamic(() => import('./client-only'), { ssr: false });

Для компонентов которые принципиально не работают в SSR (3rd-party widgets, libs использующие window).

d) suppressHydrationWarning (последний приём)

<span suppressHydrationWarning>{new Date().toLocaleString()}</span>

Подавляет warning, но не фиксит mismatch. Используй ТОЛЬКО когда:

  • Знаешь что content разный намеренно (timestamp, formatted date)
  • Mismatch contained в одном элементе
  • Не на root layout (browser-extensions исключение)

5. Next.js App Router specifics

Server vs Client components boundary

Server components не имеют hydration вообще. Client components имеют. Граница — "use client".

Если render-функция server-component выдаёт разный результат при двух вызовах → не hydration баг, а кэш-баг (re-fetch, revalidate).

Streaming SSR

<Suspense> boundaries позволяют разным частям hydrate в разное время. Mismatch в одной части не валит всю страницу.

Props serialization

Что передаётся из server-component в client-component → должно быть serializable. Functions / Dates / Symbols / Maps → нельзя. Если попробуешь — error в render.

6. Anti-patterns

  • ❌ Глобальный suppressHydrationWarning на root — заглушает реальные баги
  • if (typeof window !== 'undefined') без mount-gate — first client render всё равно undefined
  • 'use client' на каждом компоненте «чтобы не было SSR проблем» — теряешь весь смысл server components
  • new Date().toLocaleString() без explicit locale — все используют разные locale
  • ❌ Tracking pixel inserts через innerHTML до hydration — третий-party скрипт ломает DOM до того как React его проверит
  • useEffect для каждой проблемы — производительность страдает; иногда правильный fix — передать data из server
  • ❌ Ignore warning в dev «у нас же работает» — на prod при медленной сети баг проявится

7. Output

  1. Список mismatch'ей по странице: где, что, какая причина из топ-10
  2. Fix per-mismatch: какой pattern применили
  3. Что осталось suppressed и почему (с обоснованием)
  4. Метрика: hydration warnings count до/после
  5. Lighthouse re-run: LCP / CLS / TBT не ухудшились
К подразделу «Дебаг»
Похожие промты