Кеширование скомпилированных сообщений

В Globalize форматирование сообщений реализовано через предварительную компиляцию шаблонов в функции форматирования. Каждый вызов Globalize.messageFormatter() или Globalize.formatMessage() не просто интерпретирует строку шаблона, а преобразует её в исполняемую функцию, которая затем применяется к входным данным. Именно этот этап компиляции является ключевой точкой для кеширования.


Принцип компиляции сообщений

Сообщения в Globalize задаются через ICU-подобные шаблоны:

Globalize.loadMessages({
  ru: {
    greeting: "Привет, {name}!",
    items: "У вас {count, plural, one{# элемент} few{# элемента} many{# элементов}}"
  }
});

При обращении к сообщению:

const formatter = Globalize.messageFormatter("greeting");
formatter({ name: "Иван" });

происходит трансформация строки "Привет, {name}!" в функцию, которая знает, как подставлять параметры, обрабатывать плейсхолдеры и применять правила локали.

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


Внутренний кеш форматтеров

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

  • локаль (locale)
  • идентификатор сообщения (message key)
  • дополнительные параметры (например, опции форматирования)

Структурно это можно представить как:

cache[locale][messageKey] -> compiledFormatterFunction

При повторном вызове:

const f1 = Globalize.messageFormatter("greeting");
const f2 = Globalize.messageFormatter("greeting");

f1 и f2 ссылаются на одну и ту же скомпилированную функцию, если локаль и сообщения не изменялись.


Роль messageFormatter как точки кеширования

Функция Globalize.messageFormatter() является ленивым фабричным методом. Она выполняет две задачи:

  1. Проверяет наличие уже скомпилированного форматтера в кеше
  2. Если отсутствует — компилирует и сохраняет результат
const formatter = Globalize.messageFormatter("items");

Первая операция требует парсинга ICU-синтаксиса и построения AST (абстрактного синтаксического дерева). Вторая и последующие — только вызов уже готовой функции.


Кеширование при частых вызовах formatMessage

Альтернативный способ работы:

Globalize.formatMessage("greeting", { name: "Иван" });

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

Поэтому при интенсивных вызовах предпочтительнее сохранять форматтер явно:

const greeting = Globalize.messageFormatter("greeting");

greeting({ name: "Иван" });
greeting({ name: "Мария" });

Так устраняется лишний lookup по ключу сообщения.


Переключение локали и инвалидирование кеша

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

Globalize.locale("ru");
const f1 = Globalize.messageFormatter("greeting");

Globalize.locale("en");
const f2 = Globalize.messageFormatter("greeting");

Хотя ключ сообщения одинаков, результат будет разным, поскольку:

  • различаются правила форматирования
  • могут отличаться сами сообщения
  • применяются разные CLDR-данные

Кеш разделяется по локали, поэтому смена локали автоматически приводит к работе с другим пространством кеша. Старые форматтеры не пересоздаются, но становятся логически привязанными к своей локали.


Кеширование и загрузка сообщений

До момента вызова messageFormatter сообщения должны быть загружены:

Globalize.loadMessages({
  en: { hello: "Hello" },
  ru: { hello: "Привет" }
});

Важно, что кеширование скомпилированных сообщений не заменяет этап загрузки. При обновлении сообщений:

Globalize.loadMessages({
  ru: { hello: "Здравствуйте" }
});

поведение зависит от реализации загрузки:

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

Поэтому динамическая подмена сообщений требует аккуратного управления жизненным циклом форматтеров.


Производительность и стоимость компиляции

Компиляция сообщений включает:

  • разбор ICU-выражений
  • построение дерева условий (plural, select)
  • генерацию JS-функции
  • привязку к локали

Эта операция линейно зависит от сложности сообщения. Например:

  • простая строка: минимальные накладные расходы
  • plural/select: значительное увеличение времени компиляции

Кеширование снижает нагрузку до уровня:

  • lookup в объекте
  • вызов функции

Разница становится особенно заметной при массовом рендеринге интерфейсов.


Паттерн явного кеширования форматтеров

На практике часто используется явное хранение форматтеров:

const formatters = {
  greeting: Globalize.messageFormatter("greeting"),
  items: Globalize.messageFormatter("items"),
  error: Globalize.messageFormatter("error")
};

Такой подход даёт несколько эффектов:

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

Особенно это важно в SPA и SSR-рендеринге, где одни и те же сообщения используются многократно в одном цикле рендеринга.


Кеширование в серверной среде (Node.js)

В Node.js каждый инстанс приложения обычно работает с одним набором локалей. Это позволяет использовать долговременное кеширование форматтеров на уровне процесса:

const Globalize = require("globalize");

Globalize.loadMessages(...);
Globalize.locale("ru");

const fmt = Globalize.messageFormatter("greeting");

При таком подходе кеш живёт весь жизненный цикл процесса, что делает повторные запросы к одному и тому же сообщению практически бесплатными.

Однако при мультиарендности (multi-tenant) с разными локалями возникает необходимость:

  • либо разделять Globalize-инстансы
  • либо учитывать локаль в ключе кеша
  • либо сбрасывать кеш при смене контекста пользователя

Потенциальные проблемы кеширования

Несмотря на очевидные преимущества, кеширование скомпилированных сообщений может приводить к ряду эффектов:

Утечка памяти при динамических локалях

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

Несогласованность сообщений

При изменении набора сообщений в рантайме старые форматтеры могут продолжать использовать устаревшие шаблоны.

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

Если форматтер создаётся до полной загрузки сообщений, кеш закрепляет неполный или дефолтный вариант.


Оптимизационные стратегии

На уровне архитектуры обычно применяются следующие подходы:

  • создание форматтеров на этапе инициализации приложения
  • отказ от частого вызова messageFormatter() внутри горячих циклов
  • группировка сообщений по модулям и локалям
  • явная инвалидация кеша при смене набора сообщений
  • разделение контекстов Globalize при сложной мультиязычной логике

Итоговая модель работы кеша

Логически кеш скомпилированных сообщений в Globalize можно представить как трёхуровневую систему:

  1. Исходные сообщения (JSON-структуры локалей)
  2. Скомпилированные форматтеры (функции)
  3. Вызовы форматирования (runtime execution)

Переход между уровнями происходит только один раз на сообщение в рамках конкретной локали, после чего система работает исключительно с функциями, минуя парсинг и компиляцию.