Проблема повторного создания форматтеров Intl
Объекты семейства Intl — Intl.NumberFormat,
Intl.DateTimeFormat, Intl.Collator,
Intl.RelativeTimeFormat, Intl.ListFormat —
относятся к числу наиболее «тяжёлых» объектов в JavaScript-экосистеме.
Их создание сопровождается загрузкой и подготовкой локализационных
данных ICU, разбором опций форматирования и построением внутренних
структур, оптимизированных для последующих операций.
Повторная инициализация одинаковых форматтеров в приложении приводит к избыточным затратам памяти и времени, особенно в сценариях, где форматирование выполняется в циклах, при рендеринге списков или при обработке потоков данных. Это делает актуальной задачу переиспользования уже созданных экземпляров.
Синглтон-подход в контексте Intl чаще всего реализуется
не как единственный глобальный экземпляр, а как кэширование по ключу
конфигурации: локаль + набор опций.
Создание форматтера включает несколько этапов:
Даже для простых случаев:
new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'KZT' })
объект несёт ощутимую стоимость инициализации.
При этом сам процесс форматирования после создания — относительно дешёвый. Возникает дисбаланс: дорого создавать, дешево использовать. Именно он и делает кэширование естественным решением.
Вместо классического singleton-объекта применяется «логический синглтон» — хранилище экземпляров, где каждый уникальный набор параметров соответствует одному экземпляру форматтера.
Базовая идея:
Основная сложность заключается в том, чтобы корректно и стабильно определить ключ кэша.
Простейший подход:
const cache = new Map();
function getNumberFormatter(locale, options) {
const key = locale + '|' + JSON.stringify(options || {});
if (cache.has(key)) return cache.get(key);
const formatter = new Intl.NumberFormat(locale, options);
cache.set(key, formatter);
return formatter;
}
Однако такой подход содержит несколько скрытых проблем:
Порядок ключей в объектах не гарантирует стабильность:
{ style: 'currency', currency: 'KZT' }
{ currency: 'KZT', style: 'currency' }
логически идентичны, но сериализуются по-разному.
Для устойчивого ключа требуется нормализация:
function stableStringify(obj) {
if (!obj) return '';
return Object.keys(obj)
.sort()
.map(k => `${k}:${obj[k]}`)
.join('|');
}
Это уже снижает риск дубликатов в кэше.
Практическая архитектура чаще разделяет кэши по типам
Intl.
const numberCache = new Map();
function getNumberFormatter(locale, options) {
const key = `${locale}|${stableStringify(options)}`;
if (!numberCache.has(key)) {
numberCache.set(key, new Intl.NumberFormat(locale, options));
}
return numberCache.get(key);
}
const dateCache = new Map();
function getDateFormatter(locale, options) {
const key = `${locale}|${stableStringify(options)}`;
if (!dateCache.has(key)) {
dateCache.set(key, new Intl.DateTimeFormat(locale, options));
}
return dateCache.get(key);
}
Особенность DateTimeFormat заключается в высокой
вариативности опций (weekday, month,
hour12, timeZone), что увеличивает
кардинальность ключей и размер кэша.
Intl.Collator используется для сортировки строк с учётом
локали:
const collatorCache = new Map();
function getCollator(locale, options) {
const key = `${locale}|${stableStringify(options)}`;
if (!collatorCache.has(key)) {
collatorCache.set(key, new Intl.Collator(locale, options));
}
return collatorCache.get(key);
}
Особенность коллатора — высокая стоимость создания при больших
локалях и сложных правилах сортировки (например, de,
tr, ja).
Локаль может быть задана массивом:
['ru-RU', 'en-US']
Порядок имеет значение, но часто бизнес-логика считает набор локалей множеством, а не последовательностью.
Для корректного кэширования требуется нормализация:
function normalizeLocale(locale) {
if (Array.isArray(locale)) {
return [...locale].sort().join(',');
}
return locale;
}
При масштабировании приложений вводится единый слой управления форматтерами:
const formatterRegistry = {
number: new Map(),
date: new Map(),
collator: new Map(),
};
Унифицированный доступ:
function getFormatter(type, locale, options) {
const cache = formatterRegistry[type];
const key = `${normalizeLocale(locale)}|${stableStringify(options)}`;
if (!cache.has(key)) {
let instance;
if (type === 'number') {
instance = new Intl.NumberFormat(locale, options);
} else if (type === 'date') {
instance = new Intl.DateTimeFormat(locale, options);
} else if (type === 'collator') {
instance = new Intl.Collator(locale, options);
}
cache.set(key, instance);
}
return cache.get(key);
}
Синглтон-кэш решает проблему повторного создания, но создаёт другую — накопление экземпляров.
Особенности:
Для контроля памяти применяется ограничение размера:
class LRUCache {
constructor(limit = 50) {
this.limit = limit;
this.map = new Map();
}
get(key) {
if (!this.map.has(key)) return undefined;
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);
}
}
}
Такой подход позволяет удерживать баланс между производительностью и памятью.
Экземпляры Intl:
Это делает их идеальными кандидатами для синглтон-кэша.
new Intl.NumberFormat()
эквивалентно default locale, но при кэшировании может
быть потеряна различимость между явным и неявным значением.
const cache = new Map();
без разделения типов приводит к конфликтам ключей и усложняет отладку.
Использование JSON.stringify без нормализации:
В некоторых системах форматтеры создаются заранее:
const defaultFormatters = {
ruNumber: new Intl.NumberFormat('ru-RU'),
ruDate: new Intl.DateTimeFormat('ru-RU'),
};
Этот подход фиксирует набор локалей и опций, полностью устраняя накладные расходы на runtime-инициализацию, но снижает гибкость.
В UI-библиотеках синглтон-кэш форматтеров часто размещается:
Это позволяет избежать повторной инициализации при каждом рендере компонентов.
Синглтон для Intl-форматтеров фактически представляет
собой:
Такая модель превращает дорогостоящую операцию инициализации ICU в однократный шаг, а последующее использование — в лёгкий вызов метода форматирования без дополнительных затрат на создание структуры.