Ошибки серверного рендеринга
Вы деплоили React приложение и получили ошибку гидратации на production — HTML на стороне сервера рендерится правильно, но браузер при загрузке JavaScript выбросил ошибку. Ssr для seo возникают на стыке между сервером и клиентом, когда код компонента работает по-разному, или когда данные расходятся между временем сборки и выполнением. Очень часто такие ошибки валятся именно в 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, браузер ожидал увидеть точно такой же, но получил другой.
Условный рендеринг без проверки окружения
Главная причина ошибок гидратации — условный рендеринг, который работает по-разному на сервере и в браузере. Типичный пример:
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 выбрасывает ошибку.
Правильный способ:
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.
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 изменится.
Временные данные и случайные значения
Если компонент использует текущее время или случайные числа, каждый рендер будет давать новый результат. На сервере компонент рендерит одно значение, в браузере при гидратации получается другое.
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 вызывает ошибку.
Правильно:
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.
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>;
}
Код внутри useEffect на сервере не выполняется, поэтому такое обращение к localStorage не вызывает серверную ошибку. Ошибка window is undefined возникает, если браузерный API используется во время серверного рендера или на уровне модуля.
if (typeof window !== 'undefined') {
const savedUser = window.localStorage.getItem('user');
}
На сервере условие будет ложно, код не выполнится, и user останется null. Браузер получит HTML с пустым div. После гидратации эффект загрузит пользователя из localStorage, и React попытается обновить div с новыми данными. Если это изменит структуру HTML, произойдёт ошибка.
Как это выглядит в production
В production логах ошибки гидратации часто видны как всплески в памяти сервера, потому что сервер пытается повторно рендерить упавшие компоненты, отлавливая ошибку. На стороне клиента пользователь может увидеть white screen или приложение, которое не реагирует на клики.
В мониторинге скачут метрики: • Время рендеринга на сервере растёт (из-за повторного рендеринга компонентов) • Процент успешных гидраций падает • TTFB (Time to First Byte) замедляется • React errors в браузерной консоли пользователя
Инструменты для отладки в production — это структурированные логи с номерами версий компонентов, уникальными идентификаторами каждого рендеринга на сервере и браузере. Сравнение этих логов помогает найти, где расходятся данные.
Suspense и асинхронная загрузка
React Suspense позволяет компонентам «приостанавливаться» во время загрузки данных. На сервере можно использовать Suspense с fallback, но если fallback отличается на сервере и браузере, произойдёт ошибка гидратации.
<Suspense fallback={<div>Загрузка...</div>}>
<Component />
</Suspense>
На сервере, если <Component /> ещё загружается, рендерится fallback. На браузере при гидратации ожидается увидеть тот же fallback... но если интернет быстрый и компонент уже загружен, браузер рендерит готовый контент вместо fallback. Несовпадение вызывает ошибку.
Third-party скрипты и глобальные побочные эффекты
Если в коде приложения есть импорт модуля, который сразу обращается к window или document, это вызовет крах на сервере, где window не существует. Такие скрипты часто встречаются в библиотеках для аналитики или рекламы.
// ❌ НЕПРАВИЛЬНО — внутри библиотеки
const trackingId = window.location.href;
export function Analytics() {
return <div>{trackingId}</div>;
}
Решение — отложить импорт, использовать динамический импорт или полностью обернуть логику в проверку typeof window:
function getTrackingId() {
if (typeof window !== 'undefined') {
return window.location.href;
}
return 'server';
}
Без этой проверки весь рендеринг на сервере упадёт, и приложение не отправит вообще никакой HTML.