Hydration локализованного контента в контексте SSR возникает на стыке двух факторов: недетерминированного форматирования и различий окружения выполнения между сервером и клиентом. Основной источник проблемы — Intl API, который опирается на локаль, часовой пояс и параметры среды выполнения, часто отличающиеся между Node.js и браузером.
В процессе серверного рендеринга формируется HTML с уже вычисленными строками дат, чисел и других локализованных значений. На клиенте React или другая библиотека повторно вычисляет дерево, и если результаты Intl отличаются, возникает mismatch.
Ключевые источники различий:
Локаль среды выполнения
en-US по умолчаниюru-RUЧасовой пояс
Версия ICU данных
Нестабильные параметры форматирования
locale и
timeZoneIntl API включает несколько ключевых форматтеров, поведение которых зависит от окружения:
Intl.DateTimeFormatIntl.NumberFormatIntl.RelativeTimeFormatIntl.PluralRulesIntl.DisplayNamesДаже при одинаковых входных данных результат может отличаться:
new Intl.DateTimeFormat().format(new Date())
// "5/26/2026" на сервере (en-US по умолчанию)
// "26.05.2026" в браузере (ru-RU)
Основная проблема гидратации заключается в том, что HTML, сгенерированный сервером, должен полностью совпадать с результатом первого рендера на клиенте. Любое расхождение в локализованных строках приводит к пересозданию узлов или предупреждениям о несоответствии.
Intl-форматирование становится критическим источником нестабильности, если оно выполняется в обеих средах независимо.
Одним из базовых подходов является жесткая фиксация параметров Intl:
const formatter = new Intl.DateTimeFormat("ru-RU", {
timeZone: "UTC",
dateStyle: "short"
});
formatter.format(new Date("2026-05-26T10:00:00Z"));
Ключевой принцип заключается в устранении зависимости от среды выполнения:
localetimeZoneНаиболее устойчивый подход в SSR — перенос форматирования на сервер и передача уже готовых строк в клиентский payload.
const date = new Date("2026-05-26T10:00:00Z");
const formatted = new Intl.DateTimeFormat("ru-RU", {
timeZone: "UTC",
year: "numeric",
month: "2-digit",
day: "2-digit"
}).format(date);
// передается в JSON как строка
На клиенте используется уже готовое значение без повторного вызова Intl.
Метод formatToParts позволяет получить структурированное
представление результата форматирования, что уменьшает риск различий в
строковой сборке:
const parts = new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB"
}).formatToParts(1234.56);
Результат представляет массив частей:
При гидратации это позволяет собирать UI из фиксированных токенов, снижая вероятность расхождений из-за локализационных нюансов.
Intl.NumberFormat особенно чувствителен к локали и
настройкам округления.
new Intl.NumberFormat("ru-RU", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(1234.5);
Без фиксации параметров возможны различия:
Intl.RelativeTimeFormat добавляет дополнительный уровень
нестабильности, поскольку результат зависит от текущего времени:
new Intl.RelativeTimeFormat("ru-RU").format(-1, "day");
При SSR и клиентском рендере даже небольшая задержка приводит к различию результата (например, “вчера” против “23 часа назад”).
Для стабилизации используется:
В приложениях с мультиязычностью часто используется единый источник локали, который передается как часть состояния SSR:
localetimeZonenumberingSystemcalendarВажно, что даже при наличии глобального контекста Intl продолжает использовать системные значения, если они явно не переопределены.
Гидратационные ошибки часто возникают при следующем паттерне:
Intl.DateTimeFormat("ru-RU")Intl.DateTimeFormat() без
аргументовДаже при одинаковых входных данных результат будет различен.
Одним из надежных подходов является сериализация не только данных, но и уже отформатированных представлений:
{
"dateRaw": "2026-05-26T10:00:00Z",
"dateFormatted": "26.05.2026"
}
Такой подход устраняет необходимость повторного Intl-вызова на клиенте.
Node.js может быть собран с различными режимами ICU:
Это напрямую влияет на:
Даже при идентичном коде SSR может отличаться между окружениями.
Для предотвращения гидратационных расхождений используются следующие паттерны:
localetimeZoneformatToParts для UI-композицииIntl.NumberFormat с валютой требует особой
стабильности:
new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "USD",
currencyDisplay: "symbol"
}).format(100);
Различия могут возникать из-за:
Гидратация чувствительна к моменту вычисления
Date.now(). При SSR обычно фиксируется базовое время:
new Date() без синхронизацииВ устойчивых архитектурах Intl API не используется напрямую в UI-слое. Вместо этого вводится слой форматирования:
formatDate(timestamp, locale, timeZone)formatCurrency(value, locale, currency)formatRelativeTime(delta, locale)Это позволяет:
Любое использование Intl в контексте SSR фактически становится тестом на детерминированность системы. Несовпадение строк означает наличие скрытых зависимостей от среды выполнения, локали или времени, которые не были зафиксированы явно.