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
- Открой prod (НЕ dev — там React показывает многое в overlay; в prod warnings уходят в console)
localhost?reactDevtools=trueили используй React DevTools extension- Hard reload каждой страницы
- Console: ищи
Hydration,did not match,Text content - Network: что грузится медленнее чем должно
- 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
- Список mismatch'ей по странице: где, что, какая причина из топ-10
- Fix per-mismatch: какой pattern применили
- Что осталось suppressed и почему (с обоснованием)
- Метрика: hydration warnings count до/после
- Lighthouse re-run: LCP / CLS / TBT не ухудшились
Стартовать новый Next.js проект
Создание Next.js приложения с разумными настройками: App Router, TypeScript, Tailwind, базовые компоненты, SEO.
Smoke-чеклист после деплоя
Что прокликать в первые 5-30 минут после деплоя, чтобы поймать 500-ки, 404, broken JS, сломанные формы — до того как это поймает первый пользователь.
Аудит console-ошибок при проходе
Открыть DevTools-консоль и пройтись по всем ключевым страницам. Категоризировать ошибки и warnings: что блокер, что noise, что отложить. Без этого «всё работает» — наугад.