Кэширование на стороне сервера

Причины необходимости серверного кэширования при интернационализации

При серверном рендеринге (SSR) интернационализация становится одной из наиболее дорогих операций в конвейере генерации HTML. Основные затраты связаны не столько с форматированием строк, сколько с созданием и повторным созданием объектов Intl:

  • Intl.DateTimeFormat
  • Intl.NumberFormat
  • Intl.PluralRules
  • Intl.RelativeTimeFormat

Каждый из этих объектов в JavaScript создаётся относительно дорого, особенно при большом количестве локалей и частых запросах. При SSR (например, в среде Node.js или фреймворках вроде React в составе Next.js) нагрузка масштабируется линейно с числом запросов.

Ключевая проблема заключается в повторяемости вычислений:

  • одинаковые локали создают одинаковые форматтеры;
  • одинаковые настройки времени и чисел повторяются;
  • ICU-правила не зависят от запроса, но пересоздаются каждый раз.

Это делает кэширование не оптимизацией, а необходимостью.


Базовая модель кэширования FormatJS

FormatJS предоставляет встроенную концепцию кеша через createIntlCache.

Основная идея:

  • один экземпляр кеша на процесс или запрос;
  • переиспользование форматтеров между вызовами;
  • разделение кеша по локали и типу форматирования.

Типовая структура:

import { createIntl, createIntlCache } from 'react-intl';

const cache = createIntlCache();

const intl = createIntl(
  {
    locale: 'ru-RU',
    messages: {}
  },
  cache
);

Важный принцип

Кеш в FormatJS не является глобальным. Это намеренное решение:

  • предотвращение утечек памяти при SSR;
  • изоляция запросов пользователей;
  • контроль жизненного цикла форматтеров.

Проблема глобального кеша и SSR-риски

Использование глобальных структур вида:

const globalCache = new Map();

приводит к проблемам в серверной среде:

  1. Утечка памяти

    • локали множатся при мультипользовательской нагрузке;
  2. Пересечение контекста

    • пользователь A может получить форматтер пользователя B;
  3. Непредсказуемое поведение при смене локали

Поэтому в SSR-контексте кеш должен быть:

  • либо request-scoped;
  • либо строго изолированным по locale.

Архитектура request-level кеширования

Оптимальная модель при SSR:

HTTP Request
   ↓
createIntlCache()
   ↓
createIntl(locale, messages, cache)
   ↓
Render React tree
   ↓
Destroy cache

Такой подход гарантирует:

  • отсутствие утечек;
  • стабильность локализации;
  • предсказуемость форматирования.

Кэширование форматтеров ICU

Наиболее затратная часть — создание объектов Intl.

FormatJS внутренне опирается на переиспользование:

  • DateTimeFormat
  • NumberFormat
  • PluralRules

Ключевой механизм — memoization по параметрам:

  • locale
  • options (format preset)
  • тип форматтера

Пример логики кэш-ключа:

locale + type + JSON.stringify(options)

Оптимизация через стабилизацию опций

Частая ошибка SSR-систем — создание новых объектов конфигурации:

formatDate(date, {
  year: 'numeric',
  month: 'long',
  day: '2-digit'
});

Если объект создаётся каждый раз заново, кеш теряет эффективность.

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

const DATE_FORMAT = {
  year: 'numeric',
  month: 'long',
  day: '2-digit'
};

В контексте FormatJS это увеличивает hit-rate кеша, поскольку ключи становятся стабильными.


Сегментация кеша по локали

При серверной локализации ключевой фактор — разделение по языкам.

Типовая структура кеша:

cache
 ├── ru-RU
 │    ├── DateTimeFormat
 │    ├── NumberFormat
 │    └── PluralRules
 ├── en-US
 │    ├── DateTimeFormat
 │    ├── NumberFormat

Важно:

  • нельзя смешивать локали в одном пространстве кеша;
  • локаль должна быть частью ключа.

Поведение при гидратации

В связке SSR + клиентская гидратация в React возникает проблема согласованности:

  • сервер использует один кеш;
  • клиент создаёт новый кеш;
  • возможны расхождения форматирования.

Решение:

  • сериализация локали и сообщений на сервере;
  • повторная инициализация IntlProvider на клиенте с теми же данными.

Кеширование сообщений и ICU-строк

FormatJS использует ICU MessageFormat, который компилируется в функции.

Этапы:

  1. строка сообщения
  2. парсинг ICU
  3. генерация AST
  4. компиляция в функцию

Кеширование применяется на уровне:

  • исходной строки сообщения;
  • скомпилированной функции.

Пример:

const messages = {
  welcome: "Привет, {name}"
};

При повторных рендерах:

  • не происходит повторного парсинга ICU;
  • используется закешированная функция форматирования.

Интеграция с LRU-кешированием

В высоконагруженных SSR-системах часто добавляют внешний слой LRU:

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

Пример стратегии:

  • максимум 50 локалей;
  • максимум 1000 форматтеров на тип;
  • вытеснение по least-recently-used.

Это особенно важно при:

  • мультиарендных системах;
  • глобальных SaaS-платформах;
  • динамической локализации.

Параллельные запросы и конкуренция кеша

При SSR в Node.js несколько запросов могут одновременно обращаться к одному кешу.

Проблемы:

  • race conditions при ленивой инициализации;
  • частично заполненные кеши;
  • дублирование вычислений.

Решение:

  • прединициализация кешей;
  • использование immutable структур;
  • избегание мутаций внутри кеша.

Кеширование форматтеров дат и чисел как узкое место производительности

На практике наиболее дорогие операции:

  • DateTimeFormat.format
  • NumberFormat.format

Причина:

  • ICU и локализация;
  • сложные правила склонений;
  • временные зоны.

Стратегии оптимизации:

  • повторное использование экземпляров форматтеров;
  • группировка по конфигурациям;
  • минимизация уникальных опций.

Серверный слой как единая точка формирования Intl-объектов

В зрелых архитектурах SSR слой выполняет роль фабрики:

  • создаёт кеш;
  • инициализирует IntlProvider;
  • передаёт готовый контекст вниз по дереву компонентов.

Внутри FormatJS это позволяет:

  • сократить количество инстанцирований;
  • унифицировать локализацию;
  • контролировать жизненный цикл форматтеров.

Типичные ошибки при реализации кеширования

  • создание кеша внутри каждого компонента;
  • отсутствие привязки к локали;
  • использование глобальных mutable структур;
  • пересоздание IntlProvider на каждый рендер;
  • игнорирование сериализации сообщений.

Каждая из этих ошибок приводит к:

  • деградации производительности;
  • увеличению потребления памяти;
  • нестабильности SSR-вывода.

Масштабирование кеша при высокой нагрузке

При росте трафика основная цель кеширования:

  • уменьшить CPU-bound операции ICU;
  • снизить давление на GC;
  • стабилизировать время ответа SSR.

Эффективная модель:

  • кеш на уровень запроса;
  • кеш на уровень процесса;
  • общий слой предварительно скомпилированных сообщений.

Такая иерархия минимизирует повторные вычисления и удерживает стабильную производительность даже при большом количестве локалей и параллельных SSR-запросов.