Международализация в JavaScript связана с большим количеством
вычислений: разбор локали, загрузка правил форматирования, подбор
календаря, системы нумерации, часового пояса, шаблонов дат и множества
внутренних таблиц Unicode CLDR. Создание объектов Intl.*
может быть дорогостоящей операцией, особенно при многократном выполнении
в циклах, интерфейсах с частыми обновлениями или серверных приложениях с
высокой нагрузкой.
Для снижения затрат движки JavaScript используют механизм ленивой инициализации (lazy initialization).
Ленивая инициализация — это подход, при котором объект не подготавливается полностью в момент создания. Вместо этого тяжёлые вычисления откладываются до первого реального использования.
Пример:
const formatter = new Intl.DateTimeFormat("ru-RU");
// На этом этапе часть внутренних данных
// может ещё не быть подготовлена
formatter.format(new Date());
Вызов конструктора создаёт объект, но движок может не выполнять полный расчёт параметров форматирования сразу. Реальная подготовка произойдёт только при первом вызове:
format()formatToParts()resolvedOptions()Объекты международализации работают с огромным объёмом данных:
Подготовка этих структур требует:
Если выполнять всё это немедленно, даже неиспользуемые объекты будут создавать лишнюю нагрузку.
Спецификация ECMAScript описывает поведение абстрактно, но большинство движков реализуют примерно следующую схему.
При создании:
const nf = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR"
});
движок:
При первом вызове:
nf.format(1234.56);
выполняется:
Все последующие вызовы используют уже готовое состояние.
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());
}
Теперь произойдёт реальная инициализация каждого экземпляра.
Intl API активно использует внутренние слоты (internal slots).
Например:
const dtf = new Intl.DateTimeFormat("en-US");
Внутри объект может содержать:
[[Locale]]
[[Calendar]]
[[TimeZone]]
[[Pattern]]
[[InitializedDateTimeFormat]]
На этапе ленивой инициализации часть слотов может отсутствовать или содержать временные значения.
Полная настройка выполняется позже.
Метод resolvedOptions() часто инициирует полную
инициализацию.
const formatter = new Intl.NumberFormat("fr-FR");
console.log(formatter.resolvedOptions());
Причина проста: для возврата итоговых параметров движку необходимо вычислить:
После первой подготовки 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);
}
Проблемы:
Правильнее:
const priceFormatter = new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB"
});
function formatPrice(price) {
return priceFormatter.format(price);
}
В 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"
});
}, []);
Intl.Collator особенно тяжёлый объект.
const collator = new Intl.Collator("sv-SE");
Для корректного сравнения строк движок должен:
Эти операции обычно откладываются до первого вызова:
collator.compare("a", "b");
const rtf = new Intl.RelativeTimeFormat("ru", {
numeric: "auto"
});
Настройка formatter включает:
Поэтому движок также использует lazy initialization.
const pluralRules = new Intl.PluralRules("ru");
Для русского языка требуется сложная система склонений:
pluralRules.select(1); // one
pluralRules.select(2); // few
pluralRules.select(5); // many
Правила plural categories загружаются только при необходимости.
Intl.Segmenter может быть особенно тяжёлым из-за Unicode
segmentation rules.
const segmenter = new Intl.Segmenter("ja", {
granularity: "word"
});
Для японского языка сегментация требует сложных словарей и алгоритмов.
Подготовка часто выполняется при первом вызове:
segmenter.segment(text);
Важно понимать:
const formatter = new Intl.DateTimeFormat();
и
formatter.format(date);
— это две разные по стоимости операции.
Создание объекта:
Первое использование:
Иногда полезно заранее выполнить инициализацию.
Пример:
const formatter = new Intl.DateTimeFormat("ru-RU");
formatter.format(new Date());
Это называется warm-up.
Подход применяется:
В Node.js formatter может загружать ICU-данные.
const formatter = new Intl.NumberFormat("ar-EG");
При первом использовании могут происходить:
На сервере с тысячами запросов это может стать bottleneck.
Распространённый паттерн:
const cache = new Map();
function getFormatter(locale) {
if (!cache.has(locale)) {
cache.set(locale, new Intl.NumberFormat(locale));
}
return cache.get(locale);
}
Теперь formatter:
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");
Обычно второй вызов быстрее.
Постоянное создание formatter:
for (const item of data) {
new Intl.NumberFormat().format(item);
}
создаёт:
Особенно критично в:
Intl API старается минимизировать потребление памяти.
Если formatter никогда не используется:
const formatter = new Intl.DateTimeFormat();
движок может не загружать:
Это снижает memory footprint приложения.
Поведение lazy initialization не стандартизировано полностью.
Разные движки:
могут использовать разные стратегии:
Однако общая идея остаётся одинаковой: откладывать дорогие вычисления.
formatter.formatToParts(date);
Этот метод часто тяжелее обычного format().
Причины:
Первый вызов также инициирует 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);
});
Intl formatter можно создавать внутри worker:
const formatter = new Intl.DateTimeFormat("ru-RU");
Ленивая инициализация помогает уменьшить стартовую стоимость worker-потока.
Особенно полезно при:
Неверный 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 (International Components for Unicode).
Lazy initialization помогает:
for (...) {
new Intl.NumberFormat();
}
— плохая практика.
const formatter = new Intl.NumberFormat();
— предпочтительный вариант.
Особенно важно для:
Первое форматирование может быть существенно медленнее остальных.
Особенно в критичных UI-сценариях.
Ленивая инициализация позволяет Intl API:
Без lazy initialization использование международализации в JavaScript было бы значительно тяжелее как для браузеров, так и для серверных сред.