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

В библиотеке Globalize каждый экземпляр представляет собой контекст локали, привязанный к данным CLDR и набору функций форматирования и парсинга. Такой экземпляр инкапсулирует:

  • выбранную локаль (например, ru, en, de)
  • загруженные CLDR-данные
  • подключённые модули (numbers, dates, messages и т.д.)

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

Причины необходимости кеширования

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

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

Повторная инициализация Globalize приводит к:

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

Кеширование экземпляров устраняет эти проблемы, превращая дорогую операцию инициализации в дешёвую операцию доступа к уже готовому объекту.

Базовая стратегия кеширования по локали

Наиболее распространённый подход — хранение экземпляров в структуре вида Map, где ключом выступает код локали.

import Globalize fr om "globalize";

const cache = new Map();

function getGlobalize(locale) {
  if (cache.has(locale)) {
    return cache.get(locale);
  }

  const instance = new Globalize(locale);

  cache.set(locale, instance);

  return instance;
}

Такая модель обеспечивает:

  • уникальность экземпляра на каждую локаль
  • быстрый доступ O(1)
  • отсутствие повторной инициализации

Кеширование с учётом подключённых модулей

Globalize часто используется не как монолит, а как набор функциональных областей:

  • number
  • date
  • message
  • currency

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

const cache = new Map();

function getGlobalize(locale, modulesKey) {
  const key = `${locale}:${modulesKey}`;

  if (cache.has(key)) {
    return cache.get(key);
  }

  const instance = new Globalize(locale);

  cache.set(key, instance);

  return instance;
}

modulesKey формируется из списка подключённых возможностей, например:

  • "number"
  • "number,date"
  • "number,date,message"

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

Ленивое создание экземпляров

В высоконагруженных приложениях выгодно откладывать создание экземпляра до первого использования локали.

const cache = new Map();

function getGlobalize(locale) {
  let instance = cache.get(locale);

  if (!instance) {
    instance = new Globalize(locale);
    cache.set(locale, instance);
  }

  return instance;
}

Такой подход особенно эффективен при:

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

Кеширование в Node.js-среде

В серверной среде кеш экземпляров становится глобальным для процесса. Это создаёт дополнительные особенности:

Плюсы

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

Риски

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

Пример глобального кеша:

const globalCache = new Map();

export function getGlobalize(locale) {
  if (!globalCache.has(locale)) {
    globalCache.set(locale, new Globalize(locale));
  }

  return globalCache.get(locale);
}

Ограничение размера кеша

При работе с большим числом локалей (например, пользовательский контент, мультиарендные системы) необходимо ограничивать размер кеша.

Один из вариантов — LRU-подход:

class LRUCache {
  constructor(lim it = 50) {
    this.limit = limit;
    this.map = new Map();
  }

  get(key) {
    if (!this.map.has(key)) return null;

    const value = this.map.get(key);

    this.map.delete(key);
    this.map.set(key, value);

    return value;
  }

  set(key, value) {
    if (this.map.has(key)) {
      this.map.delete(key);
    }

    this.map.set(key, value);

    if (this.map.size > this.limit) {
      const firstKey = this.map.keys().next().value;
      this.map.delete(firstKey);
    }
  }
}

const cache = new LRUCache(100);

Такой подход обеспечивает:

  • контроль потребления памяти
  • сохранение часто используемых локалей
  • автоматическое вытеснение редко используемых экземпляров

Разделение кеша по контекстам приложения

В сложных системах кеширование может разделяться на уровни:

1. Глобальный кеш

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

2. Кеш модуля

Применяется внутри конкретного сервиса (например, биллинга или аналитики).

3. Кеш запроса

Используется в рамках одного HTTP-запроса для предотвращения повторных обращений.

function createRequestContext(locale) {
  const cache = new Map();

  return {
    getGlobalize() {
      if (!cache.has(locale)) {
        cache.set(locale, new Globalize(locale));
      }

      return cache.get(locale);
    }
  };
}

Влияние кеширования на производительность

При корректной реализации кеширование экземпляров даёт:

  • снижение времени инициализации локали практически до нуля после первого обращения
  • уменьшение нагрузки на GC за счёт отсутствия повторных временных объектов
  • стабильную производительность при росте числа запросов

Основной эффект проявляется в системах, где форматирование выполняется часто:

  • финансовые отчёты
  • e-commerce каталоги
  • аналитические панели
  • мультиязычные API

Ошибки при реализации кеша

Типовые проблемы:

1. Игнорирование параметров конфигурации

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

2. Неконтролируемый рост кеша

При динамических локалях кеш может расти бесконечно.

3. Кеширование до загрузки CLDR

Если CLDR данные загружаются асинхронно, экземпляр может оказаться невалидным.

4. Глобальное состояние в многопоточности (worker_threads)

Каждый воркер должен иметь собственный кеш, иначе возникает дублирование или гонки инициализации.

Практика прогрева кеша

Для серверных приложений применяется предварительное создание наиболее популярных локалей:

const popularLocales = ["en", "ru", "de", "fr"];

popularLocales.forEach(locale => {
  cache.set(locale, new Globalize(locale));
});

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

Композиция кеша с форматтерами

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

const formattersCache = new Map();

function getNumberFormatter(locale) {
  if (!formattersCache.has(locale)) {
    const g = getGlobalize(locale);
    formattersCache.set(locale, g.numberFormatter());
  }

  return formattersCache.get(locale);
}

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