Серверный рендеринг

Серверный рендеринг с использованием Intl API требует строгого контроля над локалями, временем выполнения и детерминированностью вывода. Основная проблема SSR в контексте форматирования заключается в том, что один и тот же код может выполняться в разных средах — на сервере и в браузере — где доступны разные настройки локали, часового пояса и даже реализации ICU.

Ключевое требование серверного рендеринга — идентичность результата на сервере и клиенте. Любое расхождение в строках приводит к проблемам гидратации.

Intl API напрямую зависит от:

  • локали окружения (en-US, ru-RU, de-DE)
  • временной зоны системы
  • версии ICU (International Components for Unicode)
  • настроек среды выполнения (Node.js, edge runtime, browser)

Даже незначительное отличие, например формат числа или даты, приводит к различию HTML.

Пример проблемного поведения:

  • сервер: 1 234,56 ₽
  • клиент: 1,234.56 ₽

Причина — различие локали или параметров форматирования.

Явная локализация вместо неявной

Использование системной локали без явного указания недопустимо в SSR.

Нестабильный вариант:

new Intl.NumberFormat().format(1234.56);

Стабильный вариант:

new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'RUB'
}).format(1234.56);

Явное указание локали устраняет зависимость от окружения и обеспечивает воспроизводимость результата.

Форматирование дат и проблема часового пояса

Дата — один из наиболее частых источников расхождений между сервером и клиентом.

Основная проблема — различие часовых поясов:

  • сервер часто работает в UTC
  • браузер использует локальный часовой пояс пользователя
new Intl.DateTimeFormat('ru-RU', {
  dateStyle: 'medium',
  timeStyle: 'short'
}).format(new Date());

Без фиксации timeZone результат может различаться:

new Intl.DateTimeFormat('ru-RU', {
  timeZone: 'UTC'
}).format(new Date());

Использование фиксированной временной зоны в SSR обеспечивает предсказуемый вывод и предотвращает расхождение HTML.

Локали из запроса пользователя

В серверной среде локаль обычно извлекается из HTTP-заголовка:

Accept-Language: ru-RU,ru;q=0.9,en;q=0.8

Типичный подход — выбор первой подходящей локали:

const locale = request.headers['accept-language']?.split(',')[0] || 'en-US';

Эта локаль используется во всех форматерах на протяжении одного запроса, что обеспечивает консистентность вывода.

Кэширование экземпляров Intl

Создание экземпляров Intl дорогостоящее по сравнению с использованием готовых объектов. В SSR это критично при высокой нагрузке.

Плохая практика — создание форматера на каждое значение:

formatPrice(price) {
  return new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency: 'RUB'
  }).format(price);
}

Оптимизированный вариант — кэширование:

const formatters = new Map();

function getCurrencyFormatter(locale) {
  if (!formatters.has(locale)) {
    formatters.set(
      locale,
      new Intl.NumberFormat(locale, {
        style: 'currency',
        currency: 'RUB'
      })
    );
  }
  return formatters.get(locale);
}

Такой подход снижает нагрузку на GC и ускоряет рендеринг.

Изоляция состояния между запросами

В SSR критично избегать утечек состояния между запросами. Кэш форматеров должен быть привязан к локали и параметрам, но не содержать пользовательских данных.

Недопустимо:

  • хранить форматер с динамической валютой без сегрегации ключей
  • переиспользовать форматер с изменяемыми опциями

Корректный ключ кэша:

locale + currency + optionsHash

Проблема гидратации и текстовых расхождений

Фреймворки с гидратацией (React, Vue, Svelte) требуют, чтобы HTML сервера совпадал с результатом первого рендера клиента.

Типичные источники ошибок:

  • разный timeZone
  • различная локаль
  • использование Date.now() во время рендера
  • условное форматирование на клиенте

Даже разный пробел или порядок символов ломает гидратацию.

Форматирование относительного времени

Intl.RelativeTimeFormat часто используется для SSR-комментариев, лент и уведомлений.

const rtf = new Intl.RelativeTimeFormat('ru-RU', {
  numeric: 'auto'
});

rtf.format(-1, 'day');

Важно фиксировать единицу измерения и локаль, иначе возможны различия:

  • “вчера”
  • “1 день назад”

При разных настройках numeric результат меняется, что может привести к несоответствию SSR и клиента.

Списки и коллокация в SSR

Intl.ListFormat и Intl.Collator также требуют стабильной конфигурации.

new Intl.ListFormat('ru-RU', {
  style: 'long',
  type: 'conjunction'
}).format(['яблоки', 'груши', 'сливы']);

Коллокация влияет на сортировку:

new Intl.Collator('ru-RU', {
  sensitivity: 'base'
}).compare(a, b);

Если сортировка выполняется на сервере, она должна использовать тот же locale, что и клиент.

ICU и различия рантаймов

Node.js может работать в разных режимах ICU:

  • full ICU
  • small ICU
  • system ICU

Это влияет на:

  • доступные локали
  • корректность форматирования
  • поддержку некоторых региональных правил

В SSR это может приводить к тому, что сервер и клиент формируют разные строки даже при одинаковом коде.

Edge runtime и ограничения Intl

В edge-окружениях (Cloudflare Workers, Vercel Edge и аналогах) Intl API может быть ограничен:

  • сокращённый набор локалей
  • различия в форматировании дат
  • ограничения на ICU data

Это требует минимизации зависимости от неявного поведения и строгого указания параметров.

Стратегия единых форматеров

В устойчивой SSR-архитектуре форматеры выделяются в отдельный слой:

  • инициализация один раз на запрос
  • кэширование по локали
  • отсутствие глобального состояния между запросами

Пример структуры:

function createFormatters(locale) {
  return {
    number: new Intl.NumberFormat(locale),
    date: new Intl.DateTimeFormat(locale),
    currency: new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: 'USD'
    })
  };
}

Такой подход упрощает контроль и снижает вероятность расхождений.

Согласование серверного и клиентского форматирования

Для предотвращения расхождений используется стратегия “server-first formatting”:

  • сервер формирует финальные строки
  • клиент не пересчитывает значения при гидратации
  • параметры форматирования сериализуются вместе с данными

Пример передачи контекста:

{
  locale: 'ru-RU',
  timeZone: 'UTC',
  currency: 'RUB'
}

Клиент использует те же параметры, но не пересчитывает уже отрисованный текст.

Производительность в SSR

Intl операции дешевле, чем ручное форматирование, но при массовом рендеринге становятся значимыми.

Оптимизации:

  • кэширование форматеров
  • группировка форматирования
  • избежание повторного создания Intl объектов
  • минимизация вызовов внутри циклов рендера

Особенно критично в списках и таблицах, где тысячи элементов могут форматироваться одновременно.

Непредсказуемость системного времени

Использование Date без контроля времени ведёт к различиям между окружениями.

Проблемные случаи:

  • тесты vs production
  • сервер в UTC vs локальный браузер
  • разные контейнеры с различными часовыми поясами

Intl форматирование должно работать только на основе заранее нормализованных временных значений.