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

Международализация в JavaScript связана с большим количеством вычислений: разбор локали, загрузка правил форматирования, подбор календаря, системы нумерации, часового пояса, шаблонов дат и множества внутренних таблиц Unicode CLDR. Создание объектов Intl.* может быть дорогостоящей операцией, особенно при многократном выполнении в циклах, интерфейсах с частыми обновлениями или серверных приложениях с высокой нагрузкой.

Для снижения затрат движки JavaScript используют механизм ленивой инициализации (lazy initialization).


Суть ленивой инициализации

Ленивая инициализация — это подход, при котором объект не подготавливается полностью в момент создания. Вместо этого тяжёлые вычисления откладываются до первого реального использования.

Пример:

const formatter = new Intl.DateTimeFormat("ru-RU");

// На этом этапе часть внутренних данных
// может ещё не быть подготовлена

formatter.format(new Date());

Вызов конструктора создаёт объект, но движок может не выполнять полный расчёт параметров форматирования сразу. Реальная подготовка произойдёт только при первом вызове:

  • format()
  • formatToParts()
  • resolvedOptions()
  • других методов, требующих готового состояния.

Почему Intl API особенно нуждается в lazy initialization

Объекты международализации работают с огромным объёмом данных:

  • правила календарей;
  • локализованные шаблоны;
  • системы записи чисел;
  • plural rules;
  • форматирование валют;
  • правила сегментации текста;
  • правила сравнения строк.

Подготовка этих структур требует:

  • выделения памяти;
  • загрузки ICU-данных;
  • нормализации локали;
  • разрешения fallback-цепочек;
  • анализа options-объекта.

Если выполнять всё это немедленно, даже неиспользуемые объекты будут создавать лишнюю нагрузку.


Внутреннее устройство ленивой инициализации

Спецификация ECMAScript описывает поведение абстрактно, но большинство движков реализуют примерно следующую схему.

При создании:

const nf = new Intl.NumberFormat("de-DE", {
    style: "currency",
    currency: "EUR"
});

движок:

  1. Создаёт JS-объект.
  2. Сохраняет исходные параметры.
  3. Помечает объект как «неинициализированный полностью».
  4. Откладывает создание ICU formatter instance.

При первом вызове:

nf.format(1234.56);

выполняется:

  1. Проверка статуса инициализации.
  2. Построение внутреннего ICU formatter.
  3. Кэширование результатов.
  4. Выполнение форматирования.

Все последующие вызовы используют уже готовое состояние.


Пример отложенной подготовки

Создание без использования

for (let i = 0; i < 10000; i++) {
    new Intl.DateTimeFormat("ru-RU");
}

Несмотря на создание большого количества объектов, движок может избежать полной подготовки каждого formatter.


Создание с использованием

const formatters = [];

for (let i = 0; i < 10000; i++) {
    formatters.push(
        new Intl.DateTimeFormat("ru-RU")
    );
}

for (const formatter of formatters) {
    formatter.format(new Date());
}

Теперь произойдёт реальная инициализация каждого экземпляра.


Lazy initialization и внутренние слоты

Intl API активно использует внутренние слоты (internal slots).

Например:

const dtf = new Intl.DateTimeFormat("en-US");

Внутри объект может содержать:

[[Locale]]
[[Calendar]]
[[TimeZone]]
[[Pattern]]
[[InitializedDateTimeFormat]]

На этапе ленивой инициализации часть слотов может отсутствовать или содержать временные значения.

Полная настройка выполняется позже.


Поведение метода resolvedOptions()

Метод resolvedOptions() часто инициирует полную инициализацию.

const formatter = new Intl.NumberFormat("fr-FR");

console.log(formatter.resolvedOptions());

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

  • реальную локаль;
  • fallback;
  • систему нумерации;
  • параметры округления;
  • шаблоны форматирования.

Кэширование после инициализации

После первой подготовки formatter обычно сохраняет внутреннее состояние.

const formatter = new Intl.NumberFormat("ja-JP");

formatter.format(1000);
formatter.format(2000);
formatter.format(3000);

Первый вызов — дорогой.

Последующие — значительно дешевле.


Производительность и повторное создание объектов

Частая ошибка:

function formatPrice(price) {
    return new Intl.NumberFormat("ru-RU", {
        style: "currency",
        currency: "RUB"
    }).format(price);
}

Проблемы:

  • постоянное создание новых объектов;
  • повторная инициализация;
  • лишние аллокации;
  • повышенная нагрузка на GC.

Правильнее:

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

function formatPrice(price) {
    return priceFormatter.format(price);
}

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

В React неправильное создание formatter внутри компонента приводит к деградации производительности.

Плохо:

function Price({ value }) {
    const formatter = new Intl.NumberFormat("ru-RU", {
        style: "currency",
        currency: "RUB"
    });

    return formatter.format(value);
}

При каждом рендере создаётся новый formatter.

Лучше:

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

function Price({ value }) {
    return formatter.format(value);
}

Или:

const formatter = useMemo(() => {
    return new Intl.NumberFormat("ru-RU", {
        style: "currency",
        currency: "RUB"
    });
}, []);

Lazy initialization в Intl.Collator

Intl.Collator особенно тяжёлый объект.

const collator = new Intl.Collator("sv-SE");

Для корректного сравнения строк движок должен:

  • загрузить правила сортировки;
  • учесть чувствительность к регистру;
  • настроить Unicode collation;
  • подготовить ICU comparator.

Эти операции обычно откладываются до первого вызова:

collator.compare("a", "b");

Intl.RelativeTimeFormat и отложенная подготовка

const rtf = new Intl.RelativeTimeFormat("ru", {
    numeric: "auto"
});

Настройка formatter включает:

  • plural rules;
  • языковые шаблоны;
  • сокращённые формы;
  • категории времени.

Поэтому движок также использует lazy initialization.


Intl.PluralRules

const pluralRules = new Intl.PluralRules("ru");

Для русского языка требуется сложная система склонений:

pluralRules.select(1); // one
pluralRules.select(2); // few
pluralRules.select(5); // many

Правила plural categories загружаются только при необходимости.


Intl.Segmenter

Intl.Segmenter может быть особенно тяжёлым из-за Unicode segmentation rules.

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

Для японского языка сегментация требует сложных словарей и алгоритмов.

Подготовка часто выполняется при первом вызове:

segmenter.segment(text);

Разница между созданием и использованием

Важно понимать:

const formatter = new Intl.DateTimeFormat();

и

formatter.format(date);

— это две разные по стоимости операции.

Создание объекта:

  • относительно дешёвое;
  • может быть частично ленивым.

Первое использование:

  • потенциально дорогое;
  • инициирует полную подготовку.

Прогрев formatter

Иногда полезно заранее выполнить инициализацию.

Пример:

const formatter = new Intl.DateTimeFormat("ru-RU");

formatter.format(new Date());

Это называется warm-up.

Подход применяется:

  • перед критическими UI-операциями;
  • при SSR;
  • в высоконагруженных API;
  • в системах реального времени.

Lazy initialization и серверный JavaScript

В Node.js formatter может загружать ICU-данные.

const formatter = new Intl.NumberFormat("ar-EG");

При первом использовании могут происходить:

  • выделение памяти;
  • обращение к ICU runtime;
  • инициализация locale tables.

На сервере с тысячами запросов это может стать bottleneck.


Кэширование formatter по локали

Распространённый паттерн:

const cache = new Map();

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

    return cache.get(locale);
}

Теперь formatter:

  • создаётся один раз;
  • лениво инициализируется;
  • повторно используется.

Кэширование по набору options

const cache = new Map();

function getDateFormatter(locale, options) {
    const key = locale + JSON.stringify(options);

    if (!cache.has(key)) {
        cache.set(
            key,
            new Intl.DateTimeFormat(locale, options)
        );
    }

    return cache.get(key);
}

Стоимость первого вызова

Первый вызов formatter может быть заметно медленнее:

console.time("first");

formatter.format(date);

console.timeEnd("first");

Следующий:

console.time("second");

formatter.format(date);

console.timeEnd("second");

Обычно второй вызов быстрее.


Влияние на garbage collector

Постоянное создание formatter:

for (const item of data) {
    new Intl.NumberFormat().format(item);
}

создаёт:

  • краткоживущие объекты;
  • внутренние ICU instances;
  • нагрузку на GC.

Особенно критично в:

  • анимациях;
  • таблицах;
  • графиках;
  • виртуализированных списках.

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

Intl API старается минимизировать потребление памяти.

Если formatter никогда не используется:

const formatter = new Intl.DateTimeFormat();

движок может не загружать:

  • шаблоны дат;
  • ICU formatter;
  • locale-specific tables.

Это снижает memory footprint приложения.


Возможные различия между движками

Поведение lazy initialization не стандартизировано полностью.

Разные движки:

  • V8;
  • SpiderMonkey;
  • JavaScriptCore;

могут использовать разные стратегии:

  • eager initialization;
  • partial lazy setup;
  • shared ICU caches;
  • pooled formatter instances.

Однако общая идея остаётся одинаковой: откладывать дорогие вычисления.


Неочевидная стоимость formatToParts()

formatter.formatToParts(date);

Этот метод часто тяжелее обычного format().

Причины:

  • разбор шаблона;
  • генерация token structure;
  • выделение массивов объектов.

Первый вызов также инициирует lazy initialization.


Форматирование в циклах

Плохо:

items.map(item => {
    return new Intl.NumberFormat("en-US")
        .format(item.price);
});

Лучше:

const formatter = new Intl.NumberFormat("en-US");

items.map(item => {
    return formatter.format(item.price);
});

Lazy initialization и Web Workers

Intl formatter можно создавать внутри worker:

const formatter = new Intl.DateTimeFormat("ru-RU");

Ленивая инициализация помогает уменьшить стартовую стоимость worker-потока.

Особенно полезно при:

  • создании временных workers;
  • параллельной обработке данных;
  • фоновых вычислениях.

Подводные камни микробенчмарков

Неверный benchmark:

console.time("test");

new Intl.NumberFormat("ru-RU");

console.timeEnd("test");

Здесь измеряется только создание объекта.

Правильнее:

const formatter = new Intl.NumberFormat("ru-RU");

console.time("test");

formatter.format(12345);

console.timeEnd("test");

Именно здесь проявляется стоимость lazy initialization.


Взаимодействие с ICU

Большинство движков используют ICU (International Components for Unicode).

Lazy initialization помогает:

  • не загружать ICU раньше времени;
  • откладывать expensive locale resolution;
  • уменьшать startup cost приложения.

Практические рекомендации

Не создавать formatter внутри циклов

for (...) {
    new Intl.NumberFormat();
}

— плохая практика.


Повторно использовать экземпляры

const formatter = new Intl.NumberFormat();

— предпочтительный вариант.


Кэшировать formatter

Особенно важно для:

  • SPA;
  • SSR;
  • dashboard-интерфейсов;
  • таблиц;
  • финансовых приложений.

Учитывать стоимость первого вызова

Первое форматирование может быть существенно медленнее остальных.


Использовать warm-up при необходимости

Особенно в критичных UI-сценариях.


Архитектурное значение lazy initialization

Ленивая инициализация позволяет Intl API:

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

Без lazy initialization использование международализации в JavaScript было бы значительно тяжелее как для браузеров, так и для серверных сред.