Как исправить hydration-ошибки RSC в Next.js: практический гид
Hydration mismatch в Next.js легко поймать в dev, но в production он превращается в минимизированный код ошибки. Разбираем типичные причины и рабочий процесс отладки.
Hydration mismatch в Next.js легко поймать в next dev, но в production он превращается в минимизированный код ошибки React и ссылку на декодер. В статье разбираем, почему ошибки гидратации особенно болезненны в приложениях с React Server Components, какие причины встречаются чаще всего и какой рабочий процесс помогает находить и предотвращать такие баги.
Речь пойдёт не о теории reconcile-алгоритма, а о практике: что проверять первым делом, как изолировать проблему через Suspense, куда смотреть в production-логах и как писать тесты, которые ловят регрессии до попадания к пользователям.
Что такое hydration mismatch
Гидратация — это процесс, при котором React берёт серверный HTML и навешивает на него обработчики событий, после чего страница становится интерактивной. React ожидает, что дерево, которое он отрендерил на клиенте, точно совпадёт с тем, что пришло с сервера. Если строки, атрибуты или структура различаются, возникает hydration mismatch.
В development-режиме React покажет предупреждение, текст расхождения и стек компонента. В production всё сводится к коротким кодам вроде #418 или #425. Без телеметрии вы не узнаете, на каком маршруте, в каком компоненте и из-за каких данных произошёл сбой.
Почему это больно именно в RSC
В классическом SSR обычно один серверный рендер и один клиентский проход гидратации. App Router добавляет Server Components, Client Components, потоковую передачу, Suspense-границы, динамические данные маршрута и RSC payload, который едет рядом с HTML.
Когда несовпадение происходит вне Suspense-границы, React может отбросить весь серверный HTML и перерендерить дерево на клиенте. Приложение заплатило цену серверного рендера, а пользователь не получил ни производительности, ни преимуществ стриминга. Это не просто предупреждение в консоли, а реальная потеря производительности.
Ключевые выводы
Ключевые выводы
- Hydration mismatch в production без телеметрии почти невозможно локализовать: нужна инструментация на клиенте.
- Основные причины: browser-only API, дата/время/локаль, состояние авторизации, невалидный HTML, расширения браузера, CSS-in-JS.
- Граница 'use client' — это не только маркер сборки, но и граница гидратации: серверный рендер должен быть детерминирован.
- Suspense-границы помогают изолировать сбой и не давать ему сломать всё дерево.
- Проверяйте hydration только на production-сборке: next dev ведёт себя иначе.
- Добавьте Playwright-smoke-тесты на критичные маршруты, чтобы ловить регрессии в CI.
Самые частые причины
Перед тем как копать RSC payload или сравнивать HTML, стоит проверить шесть типовых сценариев. Они покрывают подавляющее большинство production-инцидентов.
- Browser-only API во время рендера:
window,document,localStorage,navigator— сервер не знает об этих значениях. - Дата, время и локаль:
Date,Intl.DateTimeFormat, относительное время — сервер обычно в UTC, пользователь в своей зоне. - Состояние авторизации: сервер рендерит выклогнутый UI, клиент сразу видит авторизованного пользователя.
- Невалидный HTML: браузер чинит DOM до гидратации, а React сравнивает с исходным деревом.
- Расширения и middleware: браузерные плагины и edge-переписывания меняют HTML.
- CSS-in-JS и порядок классов: styled-components и Emotion могут генерировать разные имена классов при стриминге.
Browser-only API и сторонние провайдеры
Самая частая причина — не ваш собственный код, а сторонний провайдер, который читает localStorage, window или navigator при инициализации. Под это попадают аналитика, фичер-флаги, A/B-тесты, session replay и персонализация.
Проблемный вариант: провайдер читает localStorage прямо при рендере.
Исправление: серверные флаги передаются в Client Component, а локальные оверрайды применяются после гидратации.
Принцип: серверный и первый клиентский рендер должны получить одинаковый результат. Браузерные оверрайды включаются в useEffect после монтирования.Дата, время и локаль
Сервер часто работает в UTC, а пользователь — в своей зоне. Date.toLocaleString(), Intl.DateTimeFormat и функции вроде formatDistanceToNow() дают разные строки в зависимости от часового пояса и времени между рендером и гидратацией.
Проблемный вариант: относительное время считается на сервере.
Исправление: сервер отдаёт стабильную абсолютную дату, клиент заменяет её на относительную после монтирования.
suppressHydrationWarning здесь уместен, потому что расхождение ожидаемо, ограничено листовым элементом и управляется явно. Но оборачивать им большой контейнер, чтобы скрыть неизвестную ошибку, — значит замазать баг, а не починить его.
Состояние авторизации
Типичный сценарий: сервер рендерит UI для неавторизованного пользователя, потому что маршрут статический или не читает куки, а клиент сразу видит залогиненного пользователя. При каждой загрузке страница перерендеривается.
Решение — читать куки на сервере через cookies() из next/headers.
С cookies() маршрут становится динамическим — это обычно правильный компромисс для UI, зависящего от авторизации. Начиная с Next.js 15, cookies() асинхронный и требует await. В Next.js 16 синхронный доступ к request-time API полностью уходит.
Невалидный HTML
Браузер молча чинит невалидную вёрстку. React же гидратирует не исходную строку, а DOM, который построил браузер. Классический пример — Браузер превратит это в примерно такую структуру: Если HTML приходит из CMS или редактора, валидируйте и нормализуйте его на сервере. Для собственных компонентов следите за предупреждениями validateDOMNesting в DevTools. Менеджеры паролей, переводчики, грамматические плагины и другие расширения могут вставлять или переупорядочивать узлы до гидратации. Edge middleware и CDN-трансформации тоже могут переписывать HTML в пути. Если несовпадение затрагивает только suppressHydrationWarning на корневых элементах — документированный escape hatch, но он работает только на один уровень вглубь и не должен использоваться для сокрытия неизвестных расхождений в продуктовом UI. В проектах со styled-components или Emotion имена классов могут зависеть от порядка рендера. Стриминг и Suspense меняют этот порядок, поэтому нужен registry с useServerInsertedHTML. Не начинайте с diff-а огромных HTML-документов. Работайте по порядку, сужая область поиска. Файл Если используете LogRocket, Sentry или другой инструмент, прикрепите тот же payload к текущей сессии — тогда можно будет увидеть, как выглядела страница в момент сбоя. Suspense-границы не только показывают fallback при загрузке. Если hydration mismatch случается внутри границы, React может ограничить восстановление этим поддеревом, а не переключать весь root на клиентский рендер. Оберните поочерёдно основные секции. Если страничная ошибка исчезает после оборачивания конкретной секции, баг почти наверняка внутри неё. Получите серверный HTML через curl с нужными куками: В Chrome DevTools после загрузки страницы скопируйте живой DOM: Сохраните результат в /tmp/client-render.html и сравните: Этот метод хорошо ловит HTML-слой, но может пропустить расхождения на уровне reconciler-а и RSC payload. Если raw HTML совпадает, проверяйте Client Components и браузерное состояние. Лучше не допускать ошибки, чем потом их чинить. Несколько практик, которые помогают держать серверный и клиентский рендер синхронизированными. Скелетон — это стабильная базовая разметка. Карточки товаров появляются после монтирования. Сервер и первый клиентский рендер совпадают, а пользователь получает нужный контент чуть позже. Ручное тестирование пропустит регрессии. Добавьте Playwright-тест на критичные маршруты. Запускайте такой тест только против production-сборки, никогда против next dev. Сделайте его обязательной проверкой для маршрутов, где hydration failure критичен: продуктовые страницы, дашборды, чекаут, личный кабинет. Это ситуация, когда HTML, отрендеренный на сервере, не совпадает с деревом, которое React строит на клиенте при первой гидратации. React ожидает идентичности, иначе он может отбросить серверную разметку и перерендерить UI на клиенте. В development React выводит читаемое предупреждение, текст расхождения и стек компонента. В production остаются только минимизированные коды ошибок, например #418 или #425, без указания на компонент или маршрут. Нет. Этот проп допустим только там, где расхождение ожидаемо и управляется явно, — например, для текущего времени в leaf-элементе. Если обернуть им большой контейнер, вы замаскируете баг, который продолжит портить производительность и корректность. HTML — это то, что видит браузер. RSC payload — сериализованное состояние Server Components, которое React использует для гидратации. HTML может выглядеть правильно, но payload всё равно может расходиться с тем, что клиентский компонент рендерит при гидратации. next dev использует другой путь сборки и показывает ошибки иначе, чем production. Многие hydration-баги проявляются только после next build && next start. Также локальное окружение может отличаться по таймзоне, локали, кукам и фичер-флагам. Hydration mismatch — не придирка React, а реальный баг производительности и корректности. Каждое несовпадение означает, что приложение могло отбросить серверный HTML, за который уже заплатило ресурсами. В приложениях с React Server Components, где цель — меньше клиентского JavaScript и более ранний стриминг полезного UI, hydration failures тихо отменяют эти преимущества. Поэтому важно держать браузерную логику за границей 'use client', рендерить стабильные серверные baseline и проверять гидратацию в production-условиях до того, как ошибку найдут пользователи. Источник: LogRocket Blog — How to fix RSC hydration mismatches in Next.js. Проверьте свои критичные маршруты на production-сборке, настройте телеметрию и не давайте hydration-ошибкам копиться в консоли. внутри : браузер закроет параграф до div, и React увидит другое дерево.Расширения браузера и middleware
<html> или <body>, сначала подозревайте расширения. React 19 стал терпимее к инъекциям в head/body, но на старых версиях такие ошибки особенно шумные.CSS-in-JS и порядок классов
Production workflow для отладки
Инструментация: instrumentation-client.ts
instrumentation-client.ts в Next.js запускается после загрузки документа, но до гидратации React. Это удобная точка для лёгкой клиентской телеметрии.Изоляция через Suspense
Сравнение HTML и DOM
Профилактика
Пример: стабильный baseline + клиентское улучшение
Smoke-тесты в CI
FAQ
Часто задаваемые вопросыЧто такое hydration mismatch в React?Почему в production hydration mismatch сложнее диагностировать, чем в development?Можно ли просто добавить suppressHydrationWarning и забыть?Какая разница между HTML-слоем и RSC payload?Почему next dev не подходит для проверки hydration?Выводы
Hydration errors should be fixed, not ignored — официальная позиция React.