Ошибки гидратации, возникающие из‑за неправильного преобразования данных между сервером и браузером, могут стать настоящей преградой на пути к стабильности и производительности систем. И поскольку такие проблемы могут обнаруживаться в процессе эксплуатации, важно заранее понимать их природу и быть готовыми к их устранению еще до релиза в продакшен.

Привет, Хабр. Меня зовут Степан Бурлаков. Я 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, нужно выключить фильтр с исключением ошибок, связанных с гидратацией. Это позволит значительно сократить сроки выявления проблем.

Настройка фильтров в Sentry
Настройка фильтров в Sentry

Примечание: Когда фильтр выключен, ошибки сразу видны в issues.

Воспроизведение ошибок гидратации

Воспроизведение ошибок гидратации — непростая задача. Зачастую для этого недостаточно просто открыть страницу: ошибок на весь экран вы не увидите, консоль пустая и, возможно, вы даже не поймете, что есть какие‑то проблемы.

Но к нам на помощь приходит симулятор — при работе с Mac удобно использовать встроенный Xcode Simulator. Про него не буду рассказывать, просто попробуйте, всё интуитивно понятно и просто.

Xcode Simulator
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>
  )
}

Вместо заключения

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

Поэтому не стоит бояться подобных ошибок — вооружившись пониманием их причин и нашими рекомендациями, можно повысить стабильность и качество любого сервиса.