Библиотеки интернационализации в JavaScript опираются на встроенные
API ECMAScript Internationalization API, которые предоставляют
форматирование чисел, дат, валют и текста с учётом локали. Однако
создание экземпляров форматтеров вроде Intl.NumberFormat,
Intl.DateTimeFormat и особенно компиляция ICU-шаблонов в
FormatJS — операции относительно дорогие по времени выполнения. При
высокой частоте рендеров интерфейса это становится заметным узким
местом, и ключевым способом оптимизации выступает кэширование
форматтеров.
Каждый экземпляр Intl-форматтера включает разбор локали,
нормализацию опций и подготовку внутренних таблиц правил форматирования.
Например:
const formatter = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
});
При повторном создании таких объектов без кэша происходит дублирование вычислений. В условиях React-приложений, где компоненты могут перерендериваться десятки и сотни раз, это приводит к накоплению лишней нагрузки.
В FormatJS ситуация усложняется тем, что помимо нативных форматтеров используются ICU-шаблоны сообщений, которые компилируются в функции форматирования.
FormatJS уже включает внутренние механизмы кэширования, однако их понимание важно для корректного управления производительностью.
В библиотеке react-intl кэширование реализовано через
фабрику createIntlCache:
import { createIntl, createIntlCache } fr om 'react-intl';
const cache = createIntlCache();
const intl = createIntl(
{
locale: 'ru',
messages: {
greeting: 'Привет'
}
},
cache
);
Объект cache используется для хранения:
Intl.* форматтеров;Главная идея заключается в том, что повторное использование одного и того же cache-объекта предотвращает повторную компиляцию сообщений и пересоздание форматтеров при одинаковых параметрах.
Ключ к эффективному кэшированию — комбинация
locale + options.
Для Intl.NumberFormat:
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;
}
Такой подход позволяет избежать повторного создания форматтера при идентичных параметрах.
Однако использование JSON.stringify имеет
ограничения:
Для повышения стабильности ключей применяются детерминированные стратегии:
function stableKey(locale, options = {}) {
const sortedOptions = Object.keys(options)
.sort()
.reduce((acc, key) => {
acc[key] = options[key];
return acc;
}, {});
return `${locale}:${JSON.stringify(sortedOptions)}`;
}
Такой подход устраняет проблему нестабильного порядка свойств.
FormatJS компилирует ICU-сообщения в функции, которые затем используются для форматирования строк:
import { MessageFormat } fr om 'intl-messageformat';
const msg = new MessageFormat('ru', 'Привет, {name}!');
const formatted = msg.format({ name: 'Иван' });
Компиляция шаблона — наиболее затратная операция. Поэтому кэширование
экземпляров MessageFormat даёт значительный прирост
производительности.
const messageCache = new Map();
function getMessageFormatter(locale, message) {
const key = `${locale}:${message}`;
if (messageCache.has(key)) {
return messageCache.get(key);
}
const formatter = new MessageFormat(locale, message);
messageCache.set(key, formatter);
return formatter;
}
В реальных приложениях такие кэши могут содержать тысячи шаблонов, поэтому важно контролировать их размер.
При неограниченном кэшировании возникает риск роста потребления памяти. Для контроля используется LRU (Least Recently Used) стратегия:
class LRUCache {
constructor(lim it = 500) {
this.lim it = limit;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return null;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, value);
if (this.cache.size > this.limit) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
}
Применение LRU особенно важно для серверного рендеринга, где количество уникальных комбинаций локалей и сообщений может быть большим.
В React-приложениях FormatJS часто используется через
IntlProvider. Каждый рендер может теоретически приводить к
пересозданию контекста, если cache не стабилен.
import { IntlProvider, createIntlCache } from 'react-intl';
const cache = createIntlCache();
function App({ locale, messages }) {
return (
<IntlProvider locale={locale} messages={messages} defaultLocale="en" onEr ror={() => {}} key={locale} _formatters={cache}>
...
</IntlProvider>
);
}
Ключевой момент — cache должен быть создан один раз на жизненный цикл приложения или на конкретную сессию локали. Пересоздание cache при каждом рендере уничтожает смысл оптимизации.
На серверной стороне кэширование требует изоляции между запросами. Общий глобальный кэш может привести к утечкам данных между пользователями.
Правильный подход:
function createRequestIntl(locale, messages) {
const cache = createIntlCache();
return createIntl(
{
locale,
messages
},
cache
);
}
Каждый запрос получает собственный cache-экземпляр, что обеспечивает безопасность и предсказуемость.
Intl-форматтеров в зависимости от локалиВажное наблюдение: форматтеры зависят не только от локали, но и от окружения выполнения (Node.js версия ICU, браузер).
Поэтому кэш должен учитывать как минимум:
ru-RU, en-US)NumberFormat,
DateTimeFormat)Пример универсального ключа:
function formatKey(type, locale, options) {
return `${type}:${locale}:${stableStringify(options)}`;
}
Для уменьшения риска утечек можно использовать WeakMap,
если ключом выступает объект конфигурации:
const formatterCache = new WeakMap();
function getFormatter(options) {
if (formatterCache.has(options)) {
return formatterCache.get(options);
}
const formatter = new Intl.NumberFormat(options.locale, options);
formatterCache.set(options, formatter);
return formatter;
}
Ограничение этого подхода заключается в невозможности использовать примитивные ключи и в сложности управления повторяемостью конфигураций.
Intl.DateTimeFormat является одним из самых дорогих
форматтеров из-за сложности календарных правил:
const dateCache = new Map();
function getDateFormatter(locale, options) {
const key = `${locale}:${JSON.stringify(options)}`;
if (!dateCache.has(key)) {
dateCache.set(key, new Intl.DateTimeFormat(locale, options));
}
return dateCache.get(key);
}
При частом отображении списков событий или сообщений чата такой кэш даёт значительное снижение нагрузки на main thread.
На практике используется несколько уровней:
Intl форматтеров;Комбинированный подход:
const intlCache = createIntlCache();
const formatterCache = new Map();
function getIntl(locale, messages) {
const key = locale;
if (!formatterCache.has(key)) {
formatterCache.set(
key,
createIntl({ locale, messages }, intlCache)
);
}
return formatterCache.get(key);
}
Чрезмерное кэширование приводит к ряду проблем:
Особенно критична ситуация, когда сообщения динамически генерируются или приходят с сервера в изменяемом виде — кэш может начать возвращать некорректные результаты.
При смене языка важно разделять кэш по пространствам локалей:
const localeCaches = new Map();
function getLocaleCache(locale) {
if (!localeCaches.has(locale)) {
localeCaches.set(locale, {
intl: createIntlCache(),
formatters: new Map()
});
}
return localeCaches.get(locale);
}
Это предотвращает смешивание форматтеров разных языков, особенно при динамическом переключении интерфейса.