Профилирование и бенчмарки

Измерение производительности в библиотеках интернационализации требует учета нескольких уровней: загрузка данных CLDR, инициализация Globalize, время выполнения форматирования и влияние кэширования. Неправильная методика приводит к ложным выводам, особенно при разовых измерениях, где значительную долю времени занимает холодный старт движка JavaScript.

Корректный подход опирается на разделение этапов:

  • загрузка и парсинг CLDR-данных;
  • создание экземпляра Globalize с конкретной локалью;
  • выполнение операций форматирования;
  • повторные вызовы (warm state);
  • оценка влияния GC и аллокаций.

Использование performance.now() обеспечивает достаточную точность для микробенчмарков в браузере и Node.js.

const t0 = performance.now();
Globalize.formatNumber(1234567.89);
const t1 = performance.now();

console.log("time:", t1 - t0);

Однако единичные измерения не отражают реальную картину, поэтому применяется цикл прогрева:

for (let i = 0; i < 1000; i++) {
  Globalize.formatNumber(i);
}

После прогрева фиксируется стабильное состояние JIT-компиляции и кэширования внутренних структур.


Особенности архитектуры Globalize и влияние на производительность

Globalize основан на данных CLDR и не содержит встроенной локализационной логики. Это означает, что производительность напрямую зависит от:

  • объема загруженных CLDR-веток;
  • структуры предварительно загруженных данных;
  • частоты доступа к форматерам;
  • использования кэширования внутри библиотеки.

Каждый вызов форматирования может задействовать несколько уровней поиска правил локали, особенно при работе с числовыми форматами и сообщениями ICU.

Ключевая особенность — отсутствие «магии» в рантайме: библиотека делает строго то, что определено данными CLDR, поэтому узкие места легко локализуются профилировщиком.


Профилирование загрузки CLDR-данных

Загрузка CLDR представляет собой одну из наиболее затратных фаз. Данные обычно подключаются вручную:

Globalize.load(
  require("cldr-data/main/ru/numbers"),
  require("cldr-data/supplemental/likelySubtags")
);

Основные источники задержек:

  • размер JSON-файлов CLDR;
  • парсинг JSON (CPU-bound операция);
  • рост памяти при десериализации;
  • повторные загрузки одинаковых сущностей.

Измерение времени загрузки

const start = performance.now();

Globalize.load(
  require("cldr-data/main/ru"),
  require("cldr-data/supplemental/likelySubtags")
);

const end = performance.now();
console.log("CLDR load:", end - start);

При анализе важно учитывать, что первый require может быть значительно медленнее из-за дискового I/O и кэширования модуля Node.js.


Бенчмаркинг форматирования чисел

Форматирование чисел — одна из самых частых операций. В Globalize оно включает разбор шаблонов CLDR, применение правил группировки и округления.

const numberFormatter = Globalize.numberFormatter();

const start = performance.now();
for (let i = 0; i < 100000; i++) {
  numberFormatter(i + 0.123);
}
const end = performance.now();

console.log("numbers:", end - start);

Факторы, влияющие на скорость

  • кэширование formatter-объекта (критически важно);
  • сложность формата (percent, currency, decimal);
  • локаль (разные правила группировки);
  • использование дробных частей.

Повторное создание formatter внутри цикла резко ухудшает результаты:

// антипаттерн
for (let i = 0; i < 100000; i++) {
  Globalize.numberFormatter()(i);
}

Бенчмаркинг дат и времени

Форматирование дат включает обработку календарных правил, временных зон и локалей.

const dateFormatter = Globalize.dateFormatter({ datetime: "medium" });

const start = performance.now();
for (let i = 0; i < 100000; i++) {
  dateFormatter(new Date(2020, 0, 1 + (i % 30)));
}
const end = performance.now();

console.log("dates:", end - start);

Основные источники затрат

  • преобразование Date в внутреннее представление;
  • выбор шаблона локали;
  • обработка форматов времени;
  • работа с календарными правилами.

Особенно заметна разница между форматами short, medium, long, где более длинные форматы требуют больше операций подстановки.


Профилирование сообщений ICU

Форматирование сообщений — наиболее сложный слой Globalize, так как включает интерполяцию и правила множественного числа.

const messageFormatter = Globalize.messageFormatter(
  "{count, plural, one {# файл} few {# файла} many {# файлов}}"
);

const start = performance.now();
for (let i = 0; i < 100000; i++) {
  messageFormatter({ count: i });
}
const end = performance.now();

console.log("messages:", end - start);

Узкие места

  • парсинг ICU-шаблонов (если не кэширован);
  • выбор формы множественного числа;
  • интерполяция значений;
  • создание временных объектов.

Ключевой фактор производительности — переиспользование скомпилированных formatter-ов.


Влияние кэширования

Globalize активно полагается на кэширование форматеров. При правильной архитектуре приложения:

  • formatter создается один раз;
  • используется многократно;
  • не пересоздается в циклах или рендерах.

Эффект кэширования может давать разницу в порядках величин.

Сравнение:

// медленно
for (let i = 0; i < 10000; i++) {
  Globalize.numberFormatter()(i);
}

// быстро
const f = Globalize.numberFormatter();
for (let i = 0; i < 10000; i++) {
  f(i);
}

Микробенчмарки и их ограничения

Микробенчмарки в JavaScript подвержены влиянию:

  • JIT-оптимизаций;
  • деоптимизаций при изменении типов;
  • сборщика мусора;
  • фоновой активности среды.

Для повышения достоверности применяются:

  • прогрев циклов;
  • фиксированные входные данные;
  • отключение логирования;
  • многократные прогоны с усреднением.
function bench(fn, name) {
  const runs = 10;
  let total = 0;

  for (let r = 0; r < runs; r++) {
    const start = performance.now();
    for (let i = 0; i < 100000; i++) fn(i);
    total += performance.now() - start;
  }

  console.log(name, total / runs);
}

Сравнение с Intl API

Встроенный Intl в современных движках часто работает быстрее за счет нативной реализации.

Сравнение проводится по одинаковым сценариям:

  • форматирование чисел;
  • форматирование дат;
  • plural rules.

Globalize уступает в raw-speed, но выигрывает в предсказуемости и независимости от среды выполнения.

Основные причины различий:

  • Intl использует C/C++ реализацию ICU;
  • Globalize работает поверх JavaScript и CLDR JSON;
  • дополнительный слой абстракции в Globalize.

Профилирование памяти

Производительность нельзя оценивать только по времени выполнения. Globalize может создавать значительные объемы промежуточных объектов.

Типичные источники потребления памяти:

  • CLDR JSON структуры;
  • formatter closures;
  • временные строки при интерполяции;
  • кешированные таблицы правил.

Инструменты анализа:

  • Chrome DevTools Memory Snapshot;
  • Node.js --inspect;
  • heap snapshots с сравнением поколений.

Особое внимание уделяется утечкам через:

  • бесконечное создание formatter-ов;
  • хранение локализованных строк в глобальных структурах;
  • кэш без ограничений.

Бандл и влияние размера данных CLDR

CLDR-данные существенно влияют на размер приложения. В production-сборках важно учитывать:

  • включение только нужных локалей;
  • исключение лишних категорий (units, dates, numbers);
  • разделение бандла по локалям.

Анализ проводится через:

  • webpack bundle analyzer;
  • esbuild metafile;
  • rollup visualizer.

Рост размера бандла напрямую влияет на:

  • время загрузки;
  • парсинг JS;
  • потребление памяти на старте.

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

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

  • создание formatter-ов внутри React/Vue render-функций;
  • отсутствие memoization;
  • частая смена локали;
  • повторная загрузка CLDR при навигации;
  • смешивание разных уровней форматирования.

Наиболее критичный паттерн:

// дорого
function render(value) {
  return Globalize.numberFormatter()(value);
}

Оптимальный вариант:

const formatter = Globalize.numberFormatter();

function render(value) {
  return formatter(value);
}

Стратегии оптимизации на основе профилирования

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

  • предварительное создание formatter-ов на уровне модуля;
  • разделение локалей по чанкам;
  • минимизация CLDR-данных;
  • кэширование сообщений ICU;
  • устранение динамического создания форматеров.

Профилирование показывает, что основная стоимость Globalize сосредоточена не в вычислениях, а в управлении данными и объектами JavaScript, что делает архитектурные решения более значимыми, чем микрооптимизации отдельных вызовов форматирования.