
Ошибки гидратации, возникающие из‑за неправильного преобразования данных между сервером и браузером, могут стать настоящей преградой на пути к стабильности и производительности систем. И поскольку такие проблемы могут обнаруживаться в процессе эксплуатации, важно заранее понимать их природу и быть готовыми к их устранению еще до релиза в продакшен.
Привет, Хабр. Меня зовут Степан Бурлаков. Я frontend‑разработчик в MedTech‑компании № 1 в России — СберЗдоровье. В этой статье я расскажу о природе ошибок гидратации, проанализирую их причины и последствия, а также предложу эффективные стратегии их предотвращения.
От контекста к проблеме
Один из приоритетов команды MedTech‑компании СберЗдоровье — обеспечить доступность собственных сервисов для всех конечных пользователей. В рамках реализации этой задачи, мы, среди прочего, заботимся о скорости загрузки страниц, улучшении SEO и ускорении появления интерактивности. Для этого мы используем Next.js для SSR (Server Side Rendering).
Примечание: SSR — подход, при котором сначала React‑компоненты рендерятся в HTML на сервере и отправляются в браузер, а затем на клиенте происходит гидратация — процесс, когда статичная HTML‑страница «оживает» и становится интерактивной: кнопки нажимаются, меню открывается, данные обновляются.
Но в процессе гидратации могут появиться ошибки, которые способны влиять на интерактивность страницы или даже полностью сломать её. И в какой‑то момент мы стали фиксировать, что подобные проблемы начали иногда появляться и у нас.
Возможные причины появления ошибок гидратации
Есть несколько типовых предпосылок появления ошибок гидратации. Выделю некоторые.
Несоответствие вывода HTML из‑за динамического рендеринга контента на сервере и клиенте. Например, использование
new Date()для генерации временных меток на сервере и клиенте приведет к несоответствию, так как они будут генерироваться в разное время.
function MyComponent() { const timestamp = new Date().toLocaleTimeString() return <div>{timestamp}</div> }
Несоответствие состояний между сервером и клиентом из‑за API, специфичных для браузера, таких как window, document или navigator.
function MyComponent() { if (typeof window !== 'undefined') { return <div>{window.innerWidth}</div> } return <div>Loading...</div> }
Неправильный синтаксис HTML или JSX. Например, когда с бэка приходит невалидная HTML‑разметка с незакрытыми тегами.
Примечательно, что обычно в процессе разработки проблем с гидратацией не возникает — даже если встретятся ошибки, о них быстро станет известно, поскольку Next.js сразу уведомит об этом на весь экран.

Как поняли, что есть проблема с гидратацией?
Для мониторинга ошибок, появляющихся у пользователей, мы используем Sentry. С его помощью я увидел, что после очередного обновления мажорной версии React на одном из проектов количество сессий без сбоев (метрика CrashFreeRate) стала падать. При этом явные ошибки (issues) в Sentry, которые могли так повлиять на стабильность, отсутствовали. Без информации и понимания, что вообще происходит у пользователей, обновить проект было нельзя.
После долгого анализа сессий пользователей я заметил минифицированные ошибки, связанные с гидратацией, при этом зачастую ошибки возникали только у пользователей на iPhone.

Примечание: Минифицированные ошибки — это сообщения об ошибках в среде выполнения (браузер, сервер, мобильное приложение), которые возникают в минифицированной (сжатой) версии кода.
Чтобы избавиться от этих «слепых зон» и найти решение для их устранения, нам изначально предстоит их воспроизвести.
К слову для того, чтобы увидеть ошибки гидратации в issues в Sentry, нужно выключить фильтр с исключением ошибок, связанных с гидратацией. Это позволит значительно сократить сроки выявления проблем.

Примечание: Когда фильтр выключен, ошибки сразу видны в issues.
Воспроизведение ошибок гидратации
Воспроизведение ошибок гидратации — непростая задача. Зачастую для этого недостаточно просто открыть страницу: ошибок на весь экран вы не увидите, консоль пустая и, возможно, вы даже не поймете, что есть какие‑то проблемы.
Но к нам на помощь приходит симулятор — при работе с Mac удобно использовать встроенный Xcode Simulator. Про него не буду рассказывать, просто попробуйте, всё интуитивно понятно и просто.

Как правило, этого достаточно, чтобы увидеть ошибку в консоли, после чего можно выкатывать фиксы и радоваться.
Примечание: На симуляторе можно увидеть ошибку даже в Dev‑окружении.
Но могут также встретиться ошибки без конкретного места в коде, в таком случае можно прибегнуть к простому методу — поочередно удалять компоненты, от общих к частным, пока ошибки не перестанут появляться в консоли. Так будет понятно, где именно проблема. Дальше нужно проанализировать код, который мог вызвать ошибку — скорее всего, сразу найдутся проблемные строки.
От поиска ошибок гидратации к их исправлению
Конкретно в моём случае обнаружилось несколько ошибок гидратации. Расскажу и о них, и тех, которые также могут встречаться в практике.
Распознавание телефонных номеров в Safari iOS.
По дефолту Safari старается распознавать номера телефонов для строк из цифр. Правда, получается у него не всегда. В нашем случае под такой кейс попал ОГРН, что привело к ошибке гидратации. Для нас данная фича не принесет пользы, так как номер телефона мы размечаем сами, поэтому просто не дадим возможность это дела��ь за нас.
Чтобы отключить это поведение, необходимо добавить <meta content="telephone=no" name="format-detection" />. Заодно также можно запретить распознавать и адрес: <meta name="format-detection" content="address=no" />
Разные даты на сервере и клиенте.
При работе с датой важно обращать внимание на таймзоны — в зависимости от них дата на сервере и клиенте будет отличаться. Про это можно просто забыть и получить ошибку гидратации.
Например, у нас есть дата в формате 2023–05–17T00:00:00+03:00, пользователю хотим показать конечно такую — 17 мая 2023 г.
Вроде все просто:
const formatedDate = new Date(date).toLocaleDateString('ru-RU', { year: 'numeric', month: 'long', day: 'numeric', })
В результате мы получили то, что хотели — 17 мая 2023 г. Но с ошибкой гидратации.
Проблема скрывается в наличии таймзоны — при рендере на сервере дата будет выглядеть так: 2023–05–16T21:00:00.000Z.
Чтобы избавиться от проблемы, используем дату без таймзоны 2023–05–17T00:00:00
Разное поведение API на сервере и в браузере (new Intl, new Date).
При форматировании числа через new Intl важно проверять, как это будет работать в старых версиях iOS. Результат может отличаться от ожиданий.
Например, имеем число 22 620 275, а хотим: 22,6 млн. Изменяем формат:
const clientsCount = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 1, notation: 'compact', compactDisplay: 'short', }).format(patientsCount)
Но на iOS 16 видим такую картину:

Исправить ошибку можно, например, следующим образом:
const counter = (Math.round(patientsCount) / 1000000) .toFixed(1).replaceAll('.', ',')
Также при использовании new Date нужно помнить о возможных проблемах. Можно столкнуться со следующей:

Safari достаточно умный и встречаются интересные кейсы: в старых версиях iOS используются неразрывные пробелы.

Для устранения ошибки заменяем все последовательности пробельных символов (включая табы, переводы строк и так далее) на одиночные пробелы.
.replaceAll(/\s+/g, ' ')
Не валидный HTML, который приходит из БД.
Тут все зависит, конечно, от проекта. Если ошибки единичные, то, возможно, легко вылечились правками непосредственно в БД. Но если таких ошибок много или нет возможности изменить разметку непосредственно в базе — стоит подумать над санитайзингом разметки.
Полученные результаты и рекомендации на основе нашего опыта
В нашем случае ошибки гидратации не повлияли на интерактивность страницы и не сломали её. Основной проблемой для меня стал поиск ошибок и непонимание на начальных этапах, насколько критично ошибки влияют на пользователей.
При этом, на основе своего опыта мы смогли сформулировать несколько рекомендаций, которые могут быть полезны и вам.
Важно проверять HTML на валидность перед релизом очередной фичи. Для этого можно воспользоваться онлайн‑валидатором W3C — и это относится не только к SSR.
Хорошая практика — использовать Sentry и постоянно мониторить ошибки пользователей.
Sentry важно настраивать под текущие потребности, конкретный проект и окружение.
Важно понимать, кто конечный пользователь разрабатываемого сервиса, и определиться с поддержкой браузеров.
Постоянно вести разработку и тестирование фичей в разных браузерах, на реальных устройствах, в том числе с использованием симуляторов.
Если есть необходимость, то можно использовать рендер только на клиенте, обернув необходимый компонент в ClientOnly:
import React, { useState, useEffect } from 'react' export const ClientOnly = () => { const [isClient, setIsClient] = useState(false) useEffect(() => { setIsClient(true) }, []) if (!isClient) { return null; // or a server-side fallback } return ( <div> <p>This content only renders on the client.</p> {/* Use browser-specific APIs here */} </div> ) }
Вместо заключения
Ошибки гидратации могут казаться сложными, если нет понимания откуда они приходят, но они вполне решаемы. Главное — действовать системно: внимательно мониторить ошибки пользователей, тестировать на реальных устройствах и писать код, учитывающий различия сервера и браузера.
Поэтому не стоит бояться подобных ошибок — вооружившись пониманием их причин и нашими рекомендациями, можно повысить стабильность и качество любого сервиса.
