Интерфейсы Intl в JavaScript построены поверх ICU
(International Components for Unicode), что делает их мощными, но не
бесплатными с точки зрения производительности. При массовых операциях
ключевой фактор — стоимость создания экземпляров форматтеров.
Каждый вызов конструктора вроде:
new Intl.NumberFormat('ru-RU')
new Intl.DateTimeFormat('ru-RU')
new Intl.Collator('ru-RU')
запускает внутреннюю инициализацию ICU-структур, загрузку правил
локали, построение таблиц сортировки и правил форматирования. Это
относительно дорогая операция по сравнению с последующим вызовом метода
format.
Ключевой принцип:
создание экземпляра
Intl.*значительно дороже, чем повторное использование уже созданного объекта
При обработке массивов данных (например, миллионов чисел или дат) ошибка заключается в создании форматтера внутри цикла:
// Плохая практика
numbers.map(n => new Intl.NumberFormat('ru-RU').format(n));
Здесь каждое значение вызывает повторную инициализацию ICU, что приводит к значительным накладным расходам CPU и памяти.
Оптимальная стратегия заключается в создании экземпляра один раз и его дальнейшем использовании:
const formatter = new Intl.NumberFormat('ru-RU');
numbers.map(n => formatter.format(n));
При массовых операциях выигрыш может достигать порядков величины, особенно при больших массивах.
Аналогично для дат:
const dateFormatter = new Intl.DateTimeFormat('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
logs.map(l => dateFormatter.format(l.timestamp));
И для сортировки:
const collator = new Intl.Collator('ru-RU');
strings.sort((a, b) => collator.compare(a, b));
Важно: экземпляры Intl потокобезопасны
в контексте JavaScript-исполнения и предназначены для
переиспользования.
Производительность сильно зависит от локали и набора опций.
Создание:
new Intl.NumberFormat('en-US')
обычно быстрее, чем:
new Intl.NumberFormat('ru-RU-u-nu-cyrl')
или локалей с расширенными Unicode-расширениями, поскольку ICU должен учитывать дополнительные правила (например, числовые системы, календарные вариации).
Также опции вроде:
{ notation: 'compact' }
{ style: 'currency' }
{ timeZone: 'Asia/Almaty' }
увеличивают стоимость инициализации, так как активируют дополнительные кодовые пути ICU.
При обработке больших массивов чисел основное узкое место часто — не
сам format, а создание экземпляра форматтера.
Сравнение подходов:
function formatAll(nums) {
return nums.map(n =>
new Intl.NumberFormat('ru-RU', {
maximumFractionDigits: 2
}).format(n)
);
}
Каждая итерация:
const numberFormat = new Intl.NumberFormat('ru-RU', {
maximumFractionDigits: 2
});
function formatAll(nums) {
return nums.map(n => numberFormat.format(n));
}
Метод formatToParts возвращает структурированный
результат:
[
{ type: 'integer', value: '1' },
{ type: 'decimal', value: ',' },
{ type: 'fraction', value: '23' }
]
Он полезен при кастомном рендеринге, но имеет дополнительные накладные расходы.
При массовых операциях:
format() быстрееformatToParts() дороже из-за создания массива и
объектов частейПример:
const formatter = new Intl.NumberFormat('ru-RU');
numbers.map(n => formatter.format(n));
против:
const formatter = new Intl.NumberFormat('ru-RU');
numbers.map(n => formatter.formatToParts(n));
Разница становится заметной при десятках тысяч элементов и выше.
Intl.DateTimeFormat особенно чувствителен к параметрам
временной зоны.
const fmt = new Intl.DateTimeFormat('ru-RU', {
timeZone: 'Europe/Moscow',
dateStyle: 'medium',
timeStyle: 'short'
});
При массовом форматировании логов ключевые проблемы:
Оптимизация:
const fmt = new Intl.DateTimeFormat('ru-RU');
for (let i = 0; i < logs.length; i++) {
logs[i].formatted = fmt.format(logs[i].timestamp);
}
Если данные уже отсортированы и группируются по времени,
дополнительная оптимизация заключается в минимизации вызовов
Date:
const fmt = new Intl.DateTimeFormat('ru-RU');
for (const ts of timestamps) {
fmt.format(ts); // без промежуточных объектов
}
Intl.Collator используется для локализованной сортировки
строк.
Основная проблема производительности — не сравнение, а количество
вызовов compare.
const collator = new Intl.Collator('ru-RU');
array.sort(collator.compare);
Этот вариант быстрее, чем:
array.sort((a, b) =>
new Intl.Collator('ru-RU').compare(a, b)
);
При каждом сравнении в сортировке вызывается comparator тысячи или
миллионы раз. Повторное создание Collator внутри callback
делает алгоритм сортировки резко медленнее.
Предварительное преобразование:
const collator = new Intl.Collator('ru-RU');
const decorated = arr.map(x => ({
value: x,
key: collator.compare(x, x)
}));
Хотя этот пример упрощён, идея заключается в снижении числа сравнений через предвычисление или кеширование ключей.
При работе с несколькими локалями и конфигурациями возникает необходимость динамического создания форматтеров. Прямое создание ведёт к повторной инициализации ICU.
Решение — кеширование:
const cache = new Map();
function getNumberFormatter(locale, options) {
const key = locale + JSON.stringify(options);
if (!cache.has(key)) {
cache.set(key, new Intl.NumberFormat(locale, options));
}
return cache.get(key);
}
В массовых системах это критично при:
Каждый экземпляр Intl содержит внутренние структуры ICU.
При частом создании:
Особенно это заметно при:
for (let i = 0; i < 1e6; i++) {
new Intl.NumberFormat('ru-RU').format(i);
}
Даже если объекты быстро становятся недостижимыми, их создание создаёт нагрузку на аллокатор и сборщик мусора.
Переиспользование одного экземпляра резко снижает нагрузку:
const fmt = new Intl.NumberFormat('ru-RU');
for (let i = 0; i < 1e6; i++) {
fmt.format(i);
}
Разные JavaScript-движки оптимизируют Intl
по-разному:
formatОднако общая закономерность сохраняется:
создание экземпляра всегда дороже вызова метода
format
Некоторые движки могут оптимизировать повторное создание с одинаковыми параметрами, но это не гарантируется спецификацией.
Intl.Segmenter используется для разбиения текста на
границы слов, предложений или графем.
При обработке больших текстов типичная ошибка:
text.split('').map(() => new Intl.Segmenter('ru', { granularity: 'word' }));
Оптимально:
const segmenter = new Intl.Segmenter('ru', { granularity: 'word' });
for (const part of segmenter.segment(text)) {
// обработка
}
Здесь стоимость создания сегментера значительно выше, чем итерация по результату.
При массовой обработке данных важно не только переиспользование, но и снижение количества вызовов:
const fmt = new Intl.NumberFormat('ru-RU');
function formatBatch(nums) {
const out = new Array(nums.length);
for (let i = 0; i < nums.length; i++) {
out[i] = fmt.format(nums[i]);
}
return out;
}
// плохо
nums.map(n => fmt.format(Number(n)))
// лучше
nums.map(fmt.format)
Каждое лишнее преобразование увеличивает стоимость пайплайна.
В системах с высокой нагрузкой применяются следующие подходы:
class IntlService {
constructor(locale) {
this.number = new Intl.NumberFormat(locale);
this.date = new Intl.DateTimeFormat(locale);
}
}
let formatter;
function getFormatter() {
if (!formatter) {
formatter = new Intl.NumberFormat('ru-RU');
}
return formatter;
}
Передача готовых форматтеров в функции:
function renderList(data, formatter) {
return data.map(formatter.format);
}
При массовых операциях поведение можно свести к трём уровням затрат:
Создание экземпляра (Intl.*)
Вызов format / compare /
segment
Постобработка результата
Оптимизация Intl почти всегда сводится к управлению
жизненным циклом объектов и сокращению их количества при сохранении
корректности локализованной обработки данных.