Чеклист по mismatche гидратации в Next.js для UI, сгенерированных ИИ
Используйте этот чеклист по ошибкам гидратации в Next.js, чтобы быстро находить отличия сервер/клиент: даты, рандом, доступ к window и нестабильные рендеры.

Что такое hydration mismatch простыми словами
При серверном рендеринге Next.js сначала отсылает HTML, чтобы страница быстро отображалась. Затем React запускается в браузере и «гидратирует» этот HTML, прикрепляя обработчики событий и повторно рендеря тот же интерфейс с теми же входными данными.
Hydration mismatch возникает, когда HTML с сервера не совпадает с тем, что React генерирует при первом рендере на клиенте. React предупреждает, потому что не может быть уверен в согласованности DOM.
На практике это может проявляться как предупреждение в консоли, короткое мерцание при замене частей страницы React-ом, кнопки, которые на мгновение не реагируют, или текст, который меняется сразу после загрузки. Иногда это едва заметно. Иногда ломает формы, интерфейс авторизации и всё, что зависит от стабильной разметки.
Код UI, сгенерированный ИИ, чаще сталкивается с этим, потому что он склонен смешивать значения, доступные только в браузере, прямо в рендере без защит. Типичные примеры: чтение из window во время рендера, форматирование дат по-разному на сервере и клиенте или использование рандома для выбора цвета, ID или значения по умолчанию.
Простое правило: если сервер и браузер могут дать разные ответы — не используйте это значение напрямую при рендере.
Что обычно можно игнорировать и что нужно исправить:
- Обычно низкий риск: небольшая разница в
className, которая не влияет на раскладку или ввод. - Исправить быстро: всё, что вызывает прыжок контента, сброс полей ввода, переключение состояния авторизации или исчезновение элементов.
- Исправлять всегда: всё, что связано с идентичностью или безопасностью (например, показ неверного состояния пользователя).
Быстрая проверка: воспроизведите и изолируйте несовместимость
Предупреждение гидратации специфично: сервер отправил HTML, затем браузер попытался прикрепить React, и первый рендер на клиенте не совпал с тем, что уже на странице. Прежде чем погоняться за ним, убедитесь, что вы не видите на самом деле ошибку получения данных (пустые данные), редирект или сдвиг макета, который выглядит как гидратация.
Воспроизводите в production-билде. Режим разработки может добавлять дополнительные рендеры и предупреждения, которые запутывают причину.
Быстрый план действий:
- Соберите production-билд, запустите приложение локально и обновите страницу, которая даёт предупреждение.
- Проверьте и жёсткую перезагрузку (первичная загрузка), и клиентскую навигацию на тот же маршрут.
- Ломается только при перезагрузке: подозревайте различия SSR.
- Ломается только после навигации: подозревайте клиентское состояние или эффекты.
- Внимательно прочитайте подсказку в предупреждении React (часто указывается конкретный элемент, текстовый узел или атрибут).
- Временно убирайте части страницы, пока предупреждение не исчезнет, затем добавляйте назад, чтобы найти минимальный компонент, который всё ещё падает.
- Когда у вас есть минимальный падающий компонент, сравните, что он рендерит на сервере и на клиенте. Достаточно одной буквы различия.
Пока задача свежа, запишите:
- Маршрут и точные шаги (перезагрузка vs навигация)
- Элемент или текст, который называет React
- Совпадает ли несовместимость по содержимому (текст), атрибутам (
class,style) или структуре (лишняя обёртка) - Появляются ли данные сразу при первом отображении или позже
Типичный паттерн для AI-виджета: заголовок «Welcome, Sam» рендерится на сервере как просто «Welcome» (пользователь ещё не известен), а клиент сразу подставляет имя из localStorage. Это несовместимость, и «перезагрузка vs навигация» обычно явно её выявляет.
10-минутные быстрые проверки гидратационных несовместимостей
Самые быстрые выигрыши приходят от поиска любого значения, которое может рендериться по-разному на сервере и в браузере. На этом этапе вы не исправляете всё — вы определяете один вход, который меняется между серверным HTML и первым рендером на клиенте.
Быстрый скан, который ловит большинство багов от AI
Откройте компонент, указаный в предупреждении (или страницу) и ищите привычных подозреваемых:
Date, Math.random, window, document, navigator, localStorage, sessionStorage.
Простая процедура:
- Уменьшите страницу до минимального компонента, который всё ещё вызывает предупреждение.
- Ищите динамические значения, используемые в JSX-тексте, атрибутах или пропсах (время, случайные числа, проверки вьюпорта).
- Проверьте условный рендеринг, основанный на вьюпорте, user agent или media queries в JavaScript (а не в CSS).
- Ищите нестабильные
key-пропсы, генерируемые ID или имена классов, которые меняются между запусками. - Подставьте подозрительное значение вручную (например, фиксированную строку даты), чтобы подтвердить, что предупреждение исчезает.
Выберите самый безопасный фикс
Большинство исправлений укладывается в несколько схем:
- Сделать рендер детерминированным: вычислять значение на сервере и передавать как проп.
- Отложить значение до монтирования: рендерить плейсхолдер, затем установить состояние в
useEffect. - Защитить обращения к браузерным API: проверять
typeof window !== "undefined"перед чтением. - Перенести виджет в клиентскую сторону, если он действительно зависит от состояния браузера.
Чеклист: даты, часовые пояса и форматирование по локали
Даты — частая причина гидрационных проблем, потому что сервер и браузер могут расходиться во «времени сейчас», в часовом поясе или в настройках локали по умолчанию. Если сформатированный текст отличается хоть немного, React будет жаловаться.
Быстрые проверки
Ищите UI, который превращает «сейчас» в текст во время рендера:
new Date()илиDate.now()в рендере компонентаIntl.DateTimeFormat(...)без фиксированногоtimeZoneиlocale- Относительное время вроде «только что», вычисленное на SSR
- Таймеры или обратный отсчёт, зависящие от текущей секунды
- Сервер использует UTC, а браузер — локальный часовой пояс
Типичный пример:
“Last updated: {new Date().toLocaleString()}”
Это почти наверняка будет отличаться между сервером и клиентом.
Паттерны фикса, которые дают стабильность
Выберите подход, соответствующий реальным потребностям пользователей:
- Передавайте временную метку как данные и рендерьте именно её.
- Форматируйте с явной временной зоной (часто
UTC) и выбранной локалью. - Если нужно относительное время, рендерьте стабильный плейсхолдер на сервере, а вычисляйте после монтирования.
- Для живых часов/таймеров рендерьте начальное значение с сервера и запускайте счётчик в эффекте.
Чеклист: рандомность и недетерминированный рендер
Ещё одна частая причина простая: сервер рендерит один вариант, а браузер — другой, потому что код с «рандомом» выполнился снова.
Сначала проверьте:
Math.random()для ID, цветов, выбора карточки, аватаров или вариантов- Сортировки или тасование внутри рендера (включая
items.sort(...), который мутирует) - Ключи, создаваемые на лету (
key={Math.random()},key={Date.now()}) - Генераторы примеров контента, возвращающие разные тексты при каждом запуске
Сделайте первый рендер детерминированным:
- Предварительно вычисляйте на сервере и передавайте как проп.
- Используйте стабильные ключи из реальных ID (или индексы только для действительно статичных списков).
- Перенесите рандомность в пост-монтирование (
useEffect), чтобы она влияла только на последующие обновления клиента.
Пример: виджет «Featured templates» тасует шаблоны при рендере и использует перемешанные индексы как ключи. Сервер показывает порядок A, клиент — B, и гидратация падает. Перемешайте один раз на сервере или инициализируйте состояние из списка, предоставленного сервером, затем используйте стабильный ID как key.
Чеклист: браузерные API и проверки окружения
Несовместимости часто возникают, когда сервер рендерит одну версию, а браузер сразу рендерит другую, потому что код читает браузерные значения слишком рано.
На что обратить внимание
Просканируйте код на использование браузерных API во время рендера (включая хелперы, вызываемые в рендере): window, document, localStorage, sessionStorage, navigator.
Также следите за UI, который меняет структуру в зависимости от размера экрана. Если вы читаете window.innerWidth или matchMedia() чтобы решить, какое дерево компонентов рендерить (а не только стили), сервер угадывает, и он иногда ошибается.
Более безопасные паттерны фикса
Сделайте первый рендер детерминированным, а затем обновляйте после монтирования:
- Защитайте доступ к браузеру:
if (typeof window !== "undefined") { ... } - Перенесите чтения браузерных значений в
useEffect(илиuseLayoutEffect, когда это действительно нужно) - Рендерьте стабильный плейсхолдер на сервере, затем заменяйте его на клиенте
- Предпочитайте CSS для адаптивных изменений вместо условного рендеринга
- Если фича полностью зависит от браузера, делайте её клиентской и держите серверный вывод минимальным
Чеклист: загрузка данных и несоответствие состояния авторизации
Такие несовместимости возникают, когда сервер рендерит один «первый вид», а браузер тут же заменяет его на другой на основе кешированных данных или состояния авторизации.
Типичные триггеры:
- Сервер рендерит «0 элементов», а клиент гидрирует с элементами из localStorage или IndexedDB.
- Сервер считает пользователя вышедшим, а клиент читает токен и показывает UI для вошедшего.
- Флаги фич оцениваются по-разному (дефолты сервера vs сохранённые предпочтения).
Паттерны фикса для согласованного первого отображения:
- Используйте согласованную loading-оболочку (такая же разметка на сервере и при первом рендере на клиенте), затем подставляйте реальные данные.
- Передавайте начальные данные и состояние авторизации с сервера, чтобы клиент начинал в том же состоянии.
- Откладывайте чтение браузерных кешей до гидратации и показывайте плейсхолдер до тех пор.
- Делайте дефолты флагов одинаковыми на сервере и клиенте, затем применяйте пользовательские настройки после гидратации.
Чеклист: стили, раскладка и адаптивный рендер
Некоторые проблемы гидратации — это «те же данные, но другая структура». Сервер рендерит одну форму DOM, браузер — другую, когда узнаёт размер экрана, шрифты или измерения.
Адаптивная логика, меняющая DOM
Если ваш UI рендерит разные деревья компонентов для разных брейкпоинтов (например, «мобайл-меню» vs «десктоп-вкладки»), сервер должен угадывать.
Предпочтительнее рендерить одинаковый DOM на обеих сторонах, а менять презентацию через CSS. Если маркап менять нужно, делайте это только после монтирования.
CSS-in-JS и порядок имён классов
Неправильная SSR-настройка для некоторых библиотек стилей может давать разные className или порядок вставки между сервером и клиентом. Если предупреждение говорит про className или вы видите вспышку стилей, убедитесь, что вы используете рекомендованную SSR-конфигурацию для вашей библиотеки стилей и не генерируете стили из недетерминированных значений.
Измерения макета и загрузка шрифтов
Замеры типа getBoundingClientRect() не совпадут на сервере. Измеряйте в useEffect, рендерьте стабильный плейсхолдер сначала и применяйте изменения, зависящие от макета, после монтирования.
Пошагово: самые безопасные способы сделать страницы стабильными
Цель простая: первый HTML, который отправляет сервер, должен совпадать с тем, что браузер рендерит до выполнения эффектов.
Надёжная последовательность стабилизации:
-
Определите, что обязано совпадать. Сконцентрируйтесь на конкретном узле, на который указывает React.
-
Сделайте серверный вывод детерминированным. Предварительно вычисляйте значения на сервере и передавайте их как пропсы. Избегайте вызовов
new Date()в рендере. -
Откладывайте логику, зависящую от браузера. Всё, что требует
window,document,localStorage, размера экрана или пользовательских настроек, должно начинаться со стабильной разметки и обновляться вuseEffect, либо жить полностью в Client Component. -
Изолируйте рискованные виджеты. Если компонент действительно зависит от браузерных API или недетерминированных значений, загружайте его только на клиенте, чтобы остальная часть страницы оставалась стабильной:
import dynamic from "next/dynamic";
const ClientOnlyWidget = dynamic(() => import("./Widget"), { ssr: false });
- Используйте
suppressHydrationWarningтолько в крайнем случае. Применяйте его к небольшому, известному и безопасному тексту, где однократное несоответствие допустимо. Не скрывайте предупреждения для интерактивного UI или содержимого, зависящего от авторизации.
Распространённые ошибки, из-за которых проблема возвращается
Предупреждения гидратации часто исчезают после одного патча, но потом возвращаются, когда кто-то добавляет новый бейдж, баннер или проверку авторизации.
«Исправления», которые приносят долгосрочные проблемы:
- Отключение SSR для всей страницы или лейаута, когда нестабилен только один маленький виджет.
- Рендер «сейчас» прямо в JSX (метки времени, «только что»).
- Генерация ключей из случайных значений или времени.
- Использование
useLayoutEffectдля браузерных изменений без плана по переводу в клиентскую часть. - Рассматривать подавление предупреждений как основное решение.
Если разметка меняется в зависимости от isLoggedIn до того, как клиент узнает сессию, сначала рендерьте нейтральную оболочку, а затем подменяйте после подтверждения авторизации.
Реальный пример: AI-виджет, ломающий гидратацию
Обычная карточка в дашборде показывает «Updated 12 seconds ago» и вычисляет это с помощью Date.now(), локали пользователя и иногда предпочтительной часовой зоны из localStorage.
Это идеальный рецепт для несовместимости: сервер рендерит одну строку (серверное время, серверная локаль, нет localStorage), а браузер рендерит другую (клиентское время, клиентская локаль, сохранённые настройки).
Вот более безопасная переработка, которая сохраняет стабильный первый рендер, а затем обновляет текст после гидратации:
function UpdatedLabel({ updatedAt, initialNow, locale }: {
updatedAt: number
initialNow: number
locale: string
}) {
const [text, setText] = React.useState(() =>
formatRelative(initialNow, updatedAt, locale)
)
React.useEffect(() => {
const id = window.setInterval(() => {
setText(formatRelative(Date.now(), updatedAt, locale))
}, 1000)
return () => window.clearInterval(id)
}, [updatedAt, locale])
return <span>{text}</span>
}
Ключевая идея: сервер и клиент разделяют одно и то же initialNow и locale для первого отображения, поэтому разметка совпадает. Только потом клиент запускает тики.
Для валидации проверьте ситуации, которые вызывают расхождение:
- production-билд (не dev режим)
- жёсткая перезагрузка с отключённым кешем
- разные часовые пояса или локали
- инкогнито-сессия (нет сохранённых настроек)
Окончательная проверка и когда просить помощи
После изменения тестируйте так, будто пытаетесь сломать приложение:
- Жёсткая перезагрузка (не только клиентская навигация)
- Окно инкогнито (нет кеша, меньше расширений)
- Эмуляция медленной сети
- Другой язык браузера или часовой пояс
Полезно держать короткую заметку «правила SSR» рядом с теми частями UI, которые часто регенерируются: не обращаться к window в рендере, не вызывать Math.random() в разметке, не форматировать даты без явной временной зоны и не рендерить UI, зависящий от авторизации, пока состояние авторизации не подтверждено.
Если после очевидных фиксов несовместимости продолжают появляться, кодовая база обычно «борется» с вами — это обычная ситуация у прототипов, сгенерированных инструментами вроде Lovable, Bolt, v0, Cursor или Replit. Команды иногда привлекают FixMyMess (fixmymess.ai) для быстрого аудита, чтобы точно указать расхождение сервер/клиент и исправить нестабильные части, не отключая SSR для всего.
Часто задаваемые вопросы
What is a hydration mismatch in Next.js, in plain English?
Гидрационная несовместимость — это когда HTML, который Next.js отсылает с сервера, не совпадает с тем, что React рендерит на клиенте при первом рендере. React предупреждает, потому что не может безопасно прикрепить обработчики событий к DOM, который он не создал.
Часто это проявляется как предупреждение в консоли, кратковременное мерцание или интерфейс, который меняется сразу после загрузки.
How do I tell if the issue is SSR-related or client-state-related?
Начните с воспроизведения в production-билде, а не в режиме разработки. Затем сравните жесткую перезагрузку страницы и клиентскую навигацию на тот же маршрут.
Если проблема проявляется только при перезагрузке — скорее всего это отличие SSR и первичного рендера на клиенте. Если в основном ломается после навигации — обычно это клиентское состояние, эффекты или кешированные данные.
What are the fastest things to search for when I see a hydration warning?
Ищите всё, что может давать разный результат на сервере и в браузере во время рендера: new Date(), Date.now(), Math.random(), форматирование по локали, window, document, navigator, localStorage, sessionStorage.
Если эти значения влияют на текст, атрибуты или какие элементы рендерятся — это вероятный источник несовместимости.
Why do dates and time zones cause so many hydration mismatches?
Потому что сервер и браузер могут по-разному понимать «сейчас», временную зону и локаль. Даже односимвольное отличие в форматированной временной метке может вызывать предупреждение.
Самый безопасный подход — рендерить стабильную временную метку, полученную из данных (или значение, заданное сервером), а относительное время вычислять уже после монтирования компонента.
Why does using Math.random() in JSX break hydration?
Потому что Math.random() выполняется и на сервере, и в браузере. Если вы используете его в рендере для генерации ID, цвета, варианта или key, сервер и клиент, скорее всего, выберут разные значения.
Сделайте первый рендер детерминированным: используйте стабильные ID из данных или перенесите рандомизацию в пост-монтирование.
What’s the right way to use window or localStorage without causing a mismatch?
Чтение браузерных API во время рендера приводит к тому, что клиент рендерит другое содержимое, чем сервер. Частый пример — показ интерфейса «вошёл в систему» на основании токена из localStorage.
Практическое решение — рендерить нейтральную стабильную оболочку на сервере и заполнять состояние, зависящее от браузера, в useEffect после гидратации.
How do I avoid mismatches with auth state (logged in vs logged out UI)?
Если сервер рендерит «вышел из системы», а клиент сразу показывает «вошёл в систему» после чтения токена, React видит разные разметки. Это может ещё и сбрасывать поля форм или делать кнопки временно нерабочими.
Чистый подход — передавать начальное состояние сессии с сервера, когда это возможно, или сначала рендерить согласованную загрузочную/скелетную разметку, пока не подтвердится состояние авторизации.
Can responsive UI logic cause hydration mismatches?
Да. Если вы используете window.innerWidth или matchMedia() во время рендера, чтобы выбрать одну из двух деревьев компонентов, сервер по сути угадывает размер экрана пользователя.
Предпочтительнее рендерить одинаковый DOM на обеих сторонах и менять только презентацию через CSS. Если нужно менять разметку — делайте это после монтирования, чтобы первый рендер оставался стабильным.
When is suppressHydrationWarning acceptable, and when is it risky?
Используйте только для небольшого, неинтерактивного текста, где допустима однократная разница. Это по сути говорит React «не предупреждай здесь», а не делает интерфейс действительно согласованным.
Не применяйте на полях форм, в UI, зависящем от авторизации, или в любых элементах, влияющих на идентичность или разрешения пользователя.
What if I can’t find the mismatch in an AI-generated Next.js codebase?
Если вы убрали очевидные причины, а предупреждение продолжает появляться в разных местах, в кодовой базе, скорее всего, есть несколько источников расхождения между сервером и клиентом. Это часто встречается в прототипах, сгенерированных ИИ, где чтения из браузера, случайные ключи и форматирование дат смешаны в путях рендера.
FixMyMess (fixmymess.ai) может провести быстрый аудит, чтобы точно указать компонент и значение, вызывающие несовместимость, и исправить их, не отключая SSR для всего приложения, обычно за 48–72 часа.