
Ошибки гидратации, возникающие из‑за неправильного преобразования данных между сервером и браузером, могут стать настоящей преградой на пути к стабильности и производительности систем. И поскольку такие проблемы могут обнаруживаться в процессе эксплуатации, важно заранее понимать их природу и быть готовыми к их устранению еще до релиза в продакшен.
Привет, Хабр. Меня зовут Степан Бурлаков. Я 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>
)
}
Вместо заключения
Ошибки гидратации могут казаться сложными, если нет понимания откуда они приходят, но они вполне решаемы. Главное — действовать системно: внимательно мониторить ошибки пользователей, тестировать на реальных устройствах и писать код, учитывающий различия сервера и браузера.
Поэтому не стоит бояться подобных ошибок — вооружившись пониманием их причин и нашими рекомендациями, можно повысить стабильность и качество любого сервиса.