Производительность при массовых операциях

Интерфейсы 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)
  );
}

Каждая итерация:

  • создание объекта
  • инициализация ICU
  • выделение памяти

Оптимизированный подход

const numberFormat = new Intl.NumberFormat('ru-RU', {
  maximumFractionDigits: 2
});

function formatAll(nums) {
  return nums.map(n => numberFormat.format(n));
}

format vs formatToParts в массовых сценариях

Метод 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'
});

При массовом форматировании логов ключевые проблемы:

  • вычисление временной зоны
  • конвертация timestamp → Date
  • применение локальных правил календаря

Оптимизация:

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); // без промежуточных объектов
}

Collator и сортировки больших массивов

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);
}

В массовых системах это критично при:

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

Память и влияние garbage collector

Каждый экземпляр Intl содержит внутренние структуры ICU. При частом создании:

  • растёт давление на GC
  • увеличивается фрагментация памяти
  • возникают пики CPU при сборке мусора

Особенно это заметно при:

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);
}

Особенности оптимизации в V8 и других движках

Разные JavaScript-движки оптимизируют Intl по-разному:

  • V8 (Chrome, Node.js) активно кеширует часть ICU-структур
  • SpiderMonkey (Firefox) имеет отдельные оптимизации для format
  • JavaScriptCore (Safari) использует собственные слои кеширования

Однако общая закономерность сохраняется:

создание экземпляра всегда дороже вызова метода format

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


Массовая обработка строк с Intl.Segmenter

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)) {
  // обработка
}

Здесь стоимость создания сегментера значительно выше, чем итерация по результату.


Батчинг и минимизация вызовов Intl

При массовой обработке данных важно не только переиспользование, но и снижение количества вызовов:

Батчинг форматирования

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)

Каждое лишнее преобразование увеличивает стоимость пайплайна.


Паттерны проектирования для высоконагруженных сценариев

В системах с высокой нагрузкой применяются следующие подходы:

Singleton форматтеров

class IntlService {
  constructor(locale) {
    this.number = new Intl.NumberFormat(locale);
    this.date = new Intl.DateTimeFormat(locale);
  }
}

Lazy initialization

let formatter;

function getFormatter() {
  if (!formatter) {
    formatter = new Intl.NumberFormat('ru-RU');
  }
  return formatter;
}

Dependency injection

Передача готовых форматтеров в функции:

function renderList(data, formatter) {
  return data.map(formatter.format);
}

Итоговая модель производительности Intl API

При массовых операциях поведение можно свести к трём уровням затрат:

  1. Создание экземпляра (Intl.*)

    • самая дорогая операция
    • зависит от локали и опций
  2. Вызов format / compare / segment

    • относительно дешёвый
    • зависит от объёма данных
  3. Постобработка результата

    • преобразование строк, DOM, JSON
    • часто становится новым узким местом

Оптимизация Intl почти всегда сводится к управлению жизненным циклом объектов и сокращению их количества при сохранении корректности локализованной обработки данных.