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

Большинство объектов Intl создают внутренние структуры данных во время инициализации:

  • загружают информацию о локали;
  • подбирают правила форматирования;
  • анализируют параметры конфигурации;
  • создают внутренние таблицы для работы с датами, числами и валютами.

Создание экземпляра может быть значительно дороже, чем повторный вызов его методов. Особенно это заметно:

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

Неправильное использование Intl часто приводит к скрытым потерям производительности.


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

Неэффективный подход

const prices = [1200, 3400, 9900, 15000];

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

  console.log(formatter.format(price));
}

На каждой итерации:

  1. создаётся новый объект;
  2. повторно анализируется локаль;
  3. повторно создаются внутренние структуры форматирования.

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


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

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

const prices = [1200, 3400, 9900, 15000];

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

for (const price of prices) {
  console.log(formatter.format(price));
}

Экземпляр создаётся один раз и используется многократно.

Это:

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

Где переиспользование особенно важно

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

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

rows.forEach(row => {
  console.log(formatter.format(row.amount));
});

Без переиспользования на каждую строку создавался бы новый formatter.

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


Работа с датами

const dateFormatter = new Intl.DateTimeFormat("ru-RU", {
  dateStyle: "long"
});

events.forEach(event => {
  console.log(dateFormatter.format(event.date));
});

Intl.DateTimeFormat считается одним из самых тяжёлых объектов внутри Intl.


Форматирование в React

Плохой вариант

function Price({ value }) {
  const formatter = new Intl.NumberFormat("ru-RU");

  return formatter.format(value);
}

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


Хороший вариант

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

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

Использование useMemo

Если локаль или настройки могут меняться:

import { useMemo } fr om "react";

function Price({ value, locale }) {
  const formatter = useMemo(() => {
    return new Intl.NumberFormat(locale, {
      style: "currency",
      currency: "USD"
    });
  }, [locale]);

  return formatter.format(value);
}

formatter будет пересоздаваться только при изменении locale.


Кэширование экземпляров

Простейший кэш

const cache = new Map();

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

  return cache.get(locale);
}

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

const formatter = getFormatter("de-DE");

console.log(formatter.format(123456));

Кэширование по нескольким параметрам

Обычно formatter зависит не только от locale.

Например:

  • валюты;
  • стиля;
  • единиц измерения;
  • режима отображения.

Пример

const cache = new Map();

function getCurrencyFormatter(locale, currency) {
  const key = `${locale}-${currency}`;

  if (!cache.has(key)) {
    cache.set(
      key,
      new Intl.NumberFormat(locale, {
        style: "currency",
        currency
      })
    );
  }

  return cache.get(key);
}

Универсальный фабричный кэш

const cache = new Map();

function getFormatter(locale, options) {
  const key = JSON.stringify([locale, options]);

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

  return cache.get(key);
}

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

const formatter = getFormatter("fr-FR", {
  style: "currency",
  currency: "EUR"
});

Почему нельзя создавать formatter внутри sort

Ошибочный подход

array.sort((a, b) => {
  const collator = new Intl.Collator("ru");

  return collator.compare(a, b);
});

Во время сортировки comparator вызывается огромное количество раз.

Это приводит к множественному созданию Intl.Collator.


Правильный подход

const collator = new Intl.Collator("ru");

array.sort((a, b) => {
  return collator.compare(a, b);
});

Переиспользование Intl.Collator

Intl.Collator особенно выигрывает от повторного использования, потому что:

  • выполняет сложную локализованную сортировку;
  • анализирует языковые правила;
  • учитывает Unicode-особенности.

Пример

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

const words = ["zebra", "åke", "äpple"];

words.sort(collator.compare);

console.log(words);

Переиспользование Intl.RelativeTimeFormat

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

console.log(rtf.format(-1, "day"));
console.log(rtf.format(-2, "hour"));
console.log(rtf.format(5, "minute"));

Один formatter используется для множества операций.


Переиспользование Intl.ListFormat

const listFormatter = new Intl.ListFormat("ru", {
  style: "long",
  type: "conjunction"
});

console.log(
  listFormatter.format([
    "JavaScript",
    "TypeScript",
    "Rust"
  ])
);

Переиспользование Intl.PluralRules

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

console.log(pluralRules.select(1));
console.log(pluralRules.select(2));
console.log(pluralRules.select(5));

Singleton-подход

Иногда formatter нужен глобально всему приложению.


Пример singleton

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

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

import { currencyFormatter } fr om "./formatters.js";

console.log(
  currencyFormatter.format(100000)
);

Менеджер formatter-объектов

В крупных приложениях часто создают отдельный сервис.


Пример

class FormatterManager {
  constructor() {
    this.cache = new Map();
  }

  getNumberFormatter(locale, options) {
    const key = JSON.stringify([
      locale,
      options
    ]);

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

    return this.cache.get(key);
  }
}

Ленивое создание

Formatter создаётся только при первом запросе.

class PriceFormatter {
  #formatter = null;

  get formatter() {
    if (!this.#formatter) {
      this.#formatter =
        new Intl.NumberFormat("ru-RU", {
          style: "currency",
          currency: "RUB"
        });
    }

    return this.#formatter;
  }

  format(value) {
    return this.formatter.format(value);
  }
}

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

В Node.js formatter может жить значительно дольше, чем в браузере.

Это особенно полезно:

  • в API;
  • при SSR;
  • в генераторах отчётов;
  • при массовой обработке данных.

Пример для Express

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

app.get("/events", (req, res) => {
  const result = events.map(event => ({
    ...event,
    date: formatter.format(event.date)
  }));

  res.json(result);
});

Опасность бесконтрольного кэширования

Потенциальная проблема

const cache = new Map();

Если ключей слишком много:

  • память будет постоянно расти;
  • старые formatter не будут удаляться;
  • приложение начнёт потреблять лишние ресурсы.

Ограниченный кэш

Пример простого LRU-подхода

class FormatterCache {
  constructor(lim it = 100) {
    this.lim it = limit;
    this.cache = new Map();
  }

  get(key, factory) {
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }

    const value = factory();

    this.cache.set(key, value);

    if (this.cache.size > this.limit) {
      const firstKey =
        this.cache.keys().next().value;

      this.cache.delete(firstKey);
    }

    return value;
  }
}

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

Редкие одиночные вызовы

console.log(
  new Intl.NumberFormat("ru").format(1000)
);

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


Баланс между оптимизацией и сложностью

Избыточное кэширование может:

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

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


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

При создании formatter движок:

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

Повторное использование позволяет избежать повторной инициализации всех этих механизмов.


Практический шаблон для production-кода

const formatterCache = new Map();

export function getNumberFormatter(
  locale,
  options
) {
  const key = JSON.stringify([
    locale,
    options
  ]);

  let formatter =
    formatterCache.get(key);

  if (!formatter) {
    formatter =
      new Intl.NumberFormat(
        locale,
        options
      );

    formatterCache.set(key, formatter);
  }

  return formatter;
}

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

const formatter = getNumberFormatter(
  "ja-JP",
  {
    style: "currency",
    currency: "JPY"
  }
);

console.log(formatter.format(100000));

Производительность в циклах

Без переиспользования

console.time("bad");

for (let i = 0; i < 100000; i++) {
  new Intl.NumberFormat("en-US")
    .format(i);
}

console.timeEnd("bad");

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

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

console.time("good");

for (let i = 0; i < 100000; i++) {
  formatter.format(i);
}

console.timeEnd("good");

Разница во времени выполнения может быть очень значительной.


Общая рекомендация

Экземпляры объектов Intl следует воспринимать как тяжёлые конфигурируемые сервисы, а не как одноразовые утилиты.

Наиболее эффективная стратегия:

  1. создать formatter один раз;
  2. сохранить его;
  3. переиспользовать при повторных вызовах;
  4. контролировать размер кэша;
  5. пересоздавать объект только при изменении конфигурации.