Перейти к содержанию
Tverknit

Ошибки серверного рендеринга

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

Гидратация: браузер оживляет HTML сервера, но разная структура вызывает ошибки
Гидратация: браузер оживляет HTML сервера, но разная структура вызывает ошибки

Гидратация в 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.

Практические решения ошибок серверного рендеринга

Как быстро проверить и исправить ошибку?

Когда React-приложение выбросит ошибку гидратации, откройте DevTools инструменты отладки и найдите message о мисматче HTML в консоли. Определите компонент в коде, оберните браузер-логику в useEffect с проверкой typeof window !== 'undefined'. Используйте dynamic импорт для SSR-unsafe компонентов на стороне клиента. Перезагрузите и проверьте рендеринг.

Что выбрать: getServerSideProps или getStaticProps?

getServerSideProps запрашивает данные при каждом запросе, рендерит новый HTML каждый раз — медленнее, но свежесть гарантирована. getStaticProps генерирует HTML при сборке приложения, загрузка данных происходит быстрее, идеально, где контент редко меняется. На практике часто комбинируют: статику для основного контента, getServerSideProps для персонализированных страниц. Правильный выбор в коде значит приложение работает быстрее.

Как настроить Error Boundary и мониторинг?

Оберните компоненты React в error boundary, чтобы catch error не приводил к краху рендеринга всего приложения. На стороне клиента используйте Suspense для асинхронного получения данных с индикаторами загрузки вместо пустого экрана ошибки. Установите мониторинг с помощью логирования ошибок рендеринга и времени отклика. С помощью метрик отслеживайте критические ошибки — это требуется для любого production приложения.

Наверх