Переиспользование форматтеров

В JavaScript операции форматирования дат, чисел и валют базируются на спецификации ECMAScript Internationalization API (Intl). При работе через Globalize надстройка использует Intl-объекты, но добавляет собственный слой управления данными CLDR и фабриками форматирования.

Ключевая проблема при масштабных приложениях — частое создание форматтеров, которое приводит к лишним затратам памяти и времени. Любой вызов вида:

new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB" })

создаёт новый объект форматтера. При высокочастотных операциях (рендер списков, таблиц, отчётов) это становится узким местом.


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

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

В Globalize этот принцип реализуется через:

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

Архитектура форматтеров в Globalize

Globalize строится вокруг нескольких уровней:

  1. CLDR-данные — локализованные правила
  2. Compile layer — преобразование правил в функции
  3. Formatter factories — создание форматтеров
  4. Runtime cache — повторное использование экземпляров

Форматтер в Globalize — это не просто объект, а функция, связанная с локалью и набором параметров.


Кеширование числовых форматтеров

Числовые форматтеры — наиболее часто используемые в UI (цены, проценты, количество).

Проблема:

Globalize("ru").numberFormatter({ style: "decimal" });
Globalize("ru").numberFormatter({ style: "decimal" });

Каждый вызов потенциально создаёт новый форматтер.

Переиспользование через сохранение ссылки

const globalize = Globalize("ru");

const formatNumber = globalize.numberFormatter({
  style: "decimal",
  minimumFractionDigits: 2
});

formatNumber(1234.5);
formatNumber(6789.1);

Здесь создаётся один экземпляр функции, который используется многократно.


Кеширование валютных форматтеров

Форматирование валют особенно дорого из-за обработки символов валюты и правил округления.

const formatRuble = Globalize("ru").currencyFormatter("RUB");

formatRuble(1000);
formatRuble(2500);

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


Паттерн фабрики форматтеров

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

const formatters = (() => {
  const g = Globalize("ru");

  const cache = new Map();

  function getNumberFormatter(options) {
    const key = JSON.stringify(options);

    if (!cache.has(key)) {
      cache.set(key, g.numberFormatter(options));
    }

    return cache.get(key);
  }

  return {
    number: getNumberFormatter
  };
})();

Такой подход устраняет повторное создание форматтеров при одинаковых параметрах.


Использование мемоизации

Мемоизация особенно полезна при динамических параметрах:

function memoize(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, fn(...args));
    }
    return cache.get(key);
  };
}

const g = Globalize("ru");

const currencyFormatter = memoize((currency) =>
  g.currencyFormatter(currency)
);

const formatUSD = currencyFormatter("USD");
const formatEUR = currencyFormatter("EUR");

Повторное использование форматтеров дат

Форматирование дат часто встречается в логах, таблицах и интерфейсах.

const g = Globalize("ru");

const shortDate = g.dateFormatter({ date: "short" });
const longDate = g.dateFormatter({ date: "long" });

shortDate(new Date());
longDate(new Date());

Переиспользование критично при массовом рендере списков.


Влияние CLDR и локалей на кеширование

Каждый форматтер привязан к:

  • локали (ru, en, de)
  • загруженным CLDR-данным
  • типу форматирования

Изменение локали означает невозможность повторного использования старых форматтеров.

const ru = Globalize("ru");
const en = Globalize("en");

const ruFormatter = ru.numberFormatter();
const enFormatter = en.numberFormatter();

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


Гранулярность кеширования

При проектировании системы кеширования важно определить уровень детализации ключей:

  • только тип (currency)
  • тип + валюта (currency + RUB)
  • тип + валюта + стиль (currency + RUB + accounting)

Избыточная детализация увеличивает память, недостаточная — снижает эффективность кеша.


Ошибки повторного создания форматтеров

Частые анти-паттерны:

// Плохо: создание на каждый рендер
function render(price) {
  return Globalize("ru")
    .currencyFormatter("RUB")(price);
}

Проблема:

  • создаётся объект Globalize
  • создаётся formatter
  • повторяется при каждом вызове

Оптимальный вариант использования

const g = Globalize("ru");
const formatPrice = g.currencyFormatter("RUB");

function render(price) {
  return formatPrice(price);
}

Создание вынесено за пределы горячего пути выполнения.


Повторное использование через dependency injection

В архитектурах с DI форматтеры передаются как зависимости:

class PriceService {
  constructor(formatCurrency) {
    this.formatCurrency = formatCurrency;
  }

  format(price) {
    return this.formatCurrency(price);
  }
}

const g = Globalize("ru");
const service = new PriceService(g.currencyFormatter("RUB"));

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


Производственные сценарии кеширования

В реальных приложениях переиспользование особенно важно в:

  • таблицах с тысячами строк
  • финансовых дашбордах
  • системах логирования
  • e-commerce карточках товаров
  • графиках и аналитике

Каждое повторное создание форматтера в таких сценариях масштабируется в значительные затраты CPU.


Комбинированное кеширование

Наиболее эффективная модель:

  • глобальный кеш по локали
  • локальный кеш по типу форматтера
  • мемоизация параметров
const formatterCache = new Map();

function getFormatter(locale, type, options) {
  const key = `${locale}:${type}:${JSON.stringify(options)}`;

  if (!formatterCache.has(key)) {
    const g = Globalize(locale);
    formatterCache.set(
      key,
      g[type + "Formatter"](options)
    );
  }

  return formatterCache.get(key);
}

Память и жизненный цикл форматтеров

Форматтеры являются долгоживущими объектами. При неправильном кешировании возможны:

  • утечки памяти через Map без очистки
  • накопление редких комбинаций опций
  • рост heap при динамических локалях

Решения включают:

  • ограниченные кеши (LRU)
  • очистку при смене конфигурации приложения
  • разделение кешей по модулям

Переиспользование как базовый принцип интернационализации

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