Профилирование локализованного кода

Международный API JavaScript (Intl) обеспечивает форматирование дат, чисел, валют, сортировку строк и разбиение текста с учётом локали. За внешней простотой скрывается слой ICU-данных и механизмов выбора локали, который влияет на производительность как при инициализации, так и при выполнении операций форматирования.

Ключевые источники затрат:

Разрешение локали (locale resolution) При создании форматтеров происходит анализ списка локалей, выбор наиболее подходящей и загрузка соответствующих правил форматирования. В средах с большим количеством поддерживаемых локалей эта операция становится заметной при частом создании экземпляров.

Инициализация ICU структур Каждый форматтер (Intl.NumberFormat, Intl.DateTimeFormat, Intl.Collator, Intl.Segmenter) создаёт внутренние структуры ICU. Они включают таблицы правил, шаблоны форматирования и предрасчитанные данные.

Аллокации объектов Создание каждого нового форматтера сопровождается выделением памяти. При высокочастотных вызовах это приводит к росту нагрузки на GC.

Форматирование как отдельная операция Хотя сам вызов format() относительно дешёв, его стоимость зависит от сложности локали, количества опций и типа данных.


Основные инструменты измерения производительности Intl

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

Performance API

Используется для измерения микрозадержек:

  • performance.now() — измерение времени создания и форматирования
  • performance.mark() и performance.measure() — структурирование тестов

Node.js профилирование

  • node --prof и --prof-process для анализа hot path
  • console.profile() для Chrome DevTools

Chrome DevTools Performance

Позволяет увидеть:

  • создание объектов Intl
  • GC-паузы
  • распределение времени между скриптом и рендерингом

Цена создания форматтеров

Создание форматтера — одна из наиболее дорогостоящих операций в Intl API.

NumberFormat

const nf = new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'RUB'
});

Внутри выполняется:

  • загрузка правил валютного форматирования
  • выбор разделителей
  • построение шаблонов округления

При массовом создании (например, в цикле рендера) наблюдается линейное ухудшение производительности.


DateTimeFormat

const dtf = new Intl.DateTimeFormat('en-US', {
  dateStyle: 'full',
  timeStyle: 'long'
});

Затраты включают:

  • выбор календарной системы
  • локализация названий месяцев и дней
  • расчёт паттернов форматирования даты/времени

Особенно дорогостоящи опции dateStyle и timeStyle, так как они разворачиваются в набор детализированных правил.


Collator

const collator = new Intl.Collator('de', {
  sensitivity: 'base'
});

Создание включает:

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

Collator особенно тяжёл при использовании сложных локалей с диакритикой и нестандартными правилами сортировки.


Segmenter

const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' });

Наиболее затратный из базовых компонентов Intl:

  • анализ графемных кластеров
  • загрузка правил Unicode Text Segmentation
  • подготовка state machine для разбиения текста

Повторное использование как ключевой фактор оптимизации

Основной принцип производительного кода с Intl заключается в разделении создания и использования.

Паттерн кеширования

const formatters = {
  ruCurrency: new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency: 'RUB'
  })
};

function formatPrice(value) {
  return formatters.ruCurrency.format(value);
}

Ключевая особенность: экземпляры форматтеров являются неизменяемыми и потокобезопасными в рамках JS-движка.


Ленивая инициализация

let dtf;

function formatDate(date) {
  if (!dtf) {
    dtf = new Intl.DateTimeFormat('ru-RU', {
      dateStyle: 'medium'
    });
  }
  return dtf.format(date);
}

Подход уменьшает стартовую стоимость приложения, перенося её на первый вызов.


Фабрика форматтеров и контроль локалей

const cache = new Map();

function getNumberFormat(locale) {
  if (!cache.has(locale)) {
    cache.set(locale, new Intl.NumberFormat(locale));
  }
  return cache.get(locale);
}

При мультилингвальных системах это предотвращает экспоненциальный рост числа объектов.


Узкие места в горячих циклах

Intl становится критическим фактором производительности при использовании внутри:

  • циклов рендеринга UI
  • map/filter/reduce на больших массивах
  • виртуальных DOM-рендеров
  • серверного рендеринга (SSR)

Антипаттерн: создание внутри цикла

items.map(item =>
  new Intl.NumberFormat('ru-RU').format(item.price)
);

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

  • росту GC pressure
  • увеличению времени выполнения
  • нестабильным задержкам

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

const nf = new Intl.NumberFormat('ru-RU');

items.map(item => nf.format(item.price));

Влияние количества локалей

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

  • рост памяти на кеширование
  • стоимость первого обращения к каждой локали
  • время резолва fallback-цепочек

Механизм fallback (например, fr-CA → fr → en) добавляет дополнительную стоимость при инициализации.


Форматирование как часть пайплайна данных

В высоконагруженных системах Intl часто становится финальным этапом обработки данных:

  1. получение сырых чисел/дат
  2. бизнес-логика
  3. агрегация
  4. локализованное форматирование

Профилирование показывает, что перенос Intl внутрь бизнес-логики приводит к скрытым затратам, особенно при повторных вызовах.


Сравнение стратегий: eager vs lazy vs cached

Eager (жадная инициализация)

  • все форматтеры создаются при старте
  • высокая память
  • минимальные задержки в runtime

Lazy (отложенная инициализация)

  • низкий стартовый overhead
  • первый вызов дорогой

Cached (кеширование по ключам)

  • баланс между памятью и скоростью
  • наиболее стабильная модель для многолокальных систем

Влияние ICU и сборки движка

Производительность Intl зависит от:

  • полной или сокращённой ICU базы
  • сборки Node.js (full-icu vs small-icu)
  • браузерной реализации (V8, JavaScriptCore, SpiderMonkey)

Сокращённые ICU-сборки уменьшают память, но увеличивают стоимость fallback-обработки и ограничивают локали.


Метрики, используемые при профилировании

Основные показатели:

  • время создания форматтера (ms)
  • время форматирования одного значения (µs)
  • количество аллокаций объектов
  • частота GC пауз
  • распределение вызовов format()

Дополнительно:

  • стоимость смены локали
  • время первого вызова (cold start)
  • время повторного вызова (warm path)

Типичные узкие места в реальных приложениях

  • создание Intl.DateTimeFormat при каждом рендере компонента
  • использование Intl.NumberFormat без кеширования в таблицах
  • сортировка с Intl.Collator внутри циклов
  • сегментация текста без переиспользования Intl.Segmenter

Эти сценарии особенно заметны в интерфейсах с частыми обновлениями состояния.


Оптимизация через предварительный прогрев

Прогрев позволяет снизить задержки первого использования:

new Intl.NumberFormat('ru-RU').format(0);
new Intl.DateTimeFormat('ru-RU').format(Date.now());

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


Особенности серверного профилирования

На сервере Intl ведёт себя иначе:

  • отсутствует DOM overhead
  • выше значимость CPU-стоимости
  • важна повторяемость результатов при масштабировании

В SSR-сценариях кеширование форматтеров часто становится обязательным условием стабильной латентности.


Практика интерпретации профилей

При анализе flamegraph’ов Intl-операции обычно проявляются как:

  • всплески при создании объектов
  • равномерные небольшие вызовы format()
  • редкие, но тяжёлые GC пики

Основная цель профилирования — отделить стоимость инициализации от стоимости использования и устранить дублирование создания объектов в горячих путях выполнения.