Ошибки серверного рендеринга
Вы деплоили React приложение и получили ошибку гидратации на production — HTML на стороне сервера рендерится правильно, но браузер при загрузке JavaScript выбросил ошибку. Ошибки серверного рендеринга возникают на стыке между сервером и клиентом, когда код компонента работает по-разному, или когда данные расходятся между временем сборки и выполнением. Очень часто такие ошибки валятся именно в production, хотя в разработке всё работает идеально. В этой статье разбираем типичные ошибки рендеринга в React приложениях: откуда они берутся, где их найти в коде на помощью отладки, и как быстрее их исправить. Используя примеры из real-world, вы научитесь находить root cause любой ошибки и работать с JavaScript приложениями эффективнее.

Гидратация в React — это процесс, когда браузер берёт уже готовый HTML, который отправил сервер, и добавляет к нему интерактивность через JavaScript. Сервер рендерит статический HTML, браузер загружает JS бандл и через React.hydrateRoot() "оживляет" эту разметку, добавляя обработчики событий и подключая состояние приложения.
На первый взгляд это просто, но на практике здесь кроется множество ловушек. Ошибки гидратации возникают, когда HTML на сервере и в браузере не совпадают. React ожидает увидеть одно дерево DOM, а находит другое — и выбросил ошибку. Самая частая в консоли:
Expected the server HTML to contain a matching <div> in <main>.
Эта фраза означает, что структура разметки отличается между сервером и браузером. Сервер создал один DOM tree, браузер ожидал увидеть точно такой же, но получил другой.
Условный рендеринг без проверки окружения
Главная причина ошибок гидратации — условный рендеринг, который работает по-разному на сервере и в браузере. Типичный пример:
javascript export function MyComponent() { const isClient = typeof window !== 'undefined';
if (isClient) { return <div>Только в браузере</div>; }
return <div>На сервере и браузере</div>; }
На сервере window не существует, поэтому условие typeof window !== 'undefined' вернёт false. Функция вернёт второй div. Но когда браузер загружает JS и начинает гидратацию, window уже доступен, условие становится true, и компонент пытается рендерить первый div. Структура не совпадает — React выбросил ошибку.
Правильный способ:
javascript export function MyComponent() { const [isClient, setIsClient] = useState(false);
useEffect(() => { setIsClient(true); }, []);
if (isClient) { return <div>Только в браузере</div>; }
return <div>На сервере и браузере</div>; }
Здесь useEffect срабатывает только на клиенте после гидратации. На сервере и при начальной гидратации рендерится одинаковый код, потом React обновляет DOM на клиенте.
Состояния загрузки и данные
Ошибки часто возникают при работе с асинхронными данными. Если getServerSideProps в Next.js не вернул ожидаемые данные, или если компонент рендерит разный контент в зависимости от состояния загрузки, сервер и браузер могут создать разный HTML.
javascript export async function getServerSideProps() { const data = await fetch('https://api.example.com'); return { props: { data } }; }
export default function Page({ data }) { // ❌ НЕПРАВИЛЬНО if (!data) return <div>Загрузка...</div>; return <div>{data.title}</div>; }
На сервере данные подгружаются и передаются в props. Браузер получает правильный HTML с готовыми данными. Но если запрос на сервере провалил, data может быть null, и сервер отправляет <div>Загрузка...</div>. Браузер же после гидратации получит те же null props и попытается рендерить тот же контент. Здесь может казаться, что всё совпадает, но если во время гидратации React начнёт делать фетч данных повторно, он может вернуть другой результат, и HTML изменится.
Временные данные и случайные значения
Если компонент использует текущее время или случайные числа, каждый рендер будет даёать новый результат. На сервере компонент рендерит одно значение, в браузере при гидратации получается другое.
javascript export function Timer() { // ❌ НЕПРАВИЛЬНО const now = new Date().toLocaleString(); return <div>{now}</div>; }
Сервер выполнит код и создаст HTML с временем сервера, например 10:30:45. Браузер загружает этот HTML и видит <div>10:30:45</div>. Но при гидратации React рендерит компонент заново, и время уже другое — 10:30:50. Несовпадение структуры HTML вызывает ошибку.
Правильно:
javascript export function Timer() { const [now, setNow] = useState('');
useEffect(() => { setNow(new Date().toLocaleString()); }, []);
return <div>{now || 'Загрузка...'}</div>; }
На сервере рендерит пустой div или заглушку, браузер получает одинаковый HTML, потом useEffect заполняет состояние настоящим временем.
useEffect и побочные эффекты
useEffect срабатывает только на браузере, никогда на сервере. Если компонент зависит от эффектов, которые меняют DOM, это может вызвать ошибки гидратации. Особенно это касается работы с window.localStorage, document, браузерными API.
javascript export function UserProfile() { const [user, setUser] = useState(null);
useEffect(() => { // ❌ НЕПРАВИЛЬНО — на сервере это не выполнится const savedUser = window.localStorage.getItem('user'); setUser(JSON.parse(savedUser)); }, []);
// На сервере renderится <div>null</div> // На браузере после эффекта renderится <div>{user.name}</div> return <div>{user?.name}</div>; }
Сервер рендерит компонент без доступа к localStorage и выбросит ошибку window is undefined. Даже если обернуть в проверку:
javascript if (typeof window !== 'undefined') { const savedUser = window.localStorage.getItem('user'); }
На сервере условие будет ложно, код не выполнится, и user останется null. Браузер получит HTML с пустым div. После гидратации эффект загрузит пользователя из localStorage, и React попытается обновить div с новыми данными. Если это изменит структуру HTML, произойдёт ошибка.
Как это выглядит в production
В production логах ошибки гидратации часто видны как всплески в памяти сервера, потому что сервер пытается повторно рендерить fallen компоненты, отлавливая ошибку. На стороне клиента пользователь может увидеть white screen или приложение, которое не реагирует на клики.
В мониторинге скачут метрики: - Время рендеринга на сервере растёт (из-за переквознения компонентов) - Процент успешных гидраций падает - TTFB (Time to First Byte) замедляется - React errors в браузерной консоли пользователя
Инструменты для отладки в production — это структурированные логи с номерами версий компонентов, уникальными идентификаторами каждого рендеринга на сервере и браузере. Сравнение этих логов помогает найти, где расходятся данные.
Suspense и асинхронная загрузка
React Suspense позволяет компонентам "приостанавливаться" во время загрузки данных. На сервере можно использовать Suspense с fallback, но если fallback отличается на сервере и браузере, произойдёт ошибка гидратации.
javascript
На сервере, если <Component /> ещё загружается, рендерится fallback. На браузере при гидратации ожидается увидеть тот же fallback... но если интернет быстрый и компонент уже загружен, браузер рендерит готовый контент вместо fallback. Несовпадение вызывает ошибку.
Third-party скрипты и глобальные побочные эффекты
Если в коде приложения есть импорт модуля, который сразу обращается к window или document, это вызовет крах на сервере, где window не существует. Такие скрипты часто встречаются в библиотеках для аналитики или рекламы.
javascript // ❌ НЕПРАВИЛЬНО — внутри библиотеки const trackingId = window.location.href;
export function Analytics() { return <div>{trackingId}</div>; }
Решение — отложить импорт, использовать динамический импорт или полностью обернуть логику в проверку typeof window:
javascript function getTrackingId() { if (typeof window !== 'undefined') { return window.location.href; } return 'server'; }
Без этой проверки весь рендеринг на сервере упадёт, и приложение не отправит вообще никакой HTML.