Наиболее распространённая ошибка при работе с Intl API —
создание новых экземпляров форматтеров внутри часто вызываемых функций,
циклов или компонентов интерфейса.
Проблемный пример:
function formatPrice(price) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(price);
}
При каждом вызове функции создаётся новый объект
Intl.NumberFormat. Для единичных операций это незаметно,
однако при обработке больших массивов данных, рендеринге таблиц или
частом обновлении интерфейса нагрузка становится существенной.
Оптимизированный вариант:
const priceFormatter = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
});
function formatPrice(price) {
return priceFormatter.format(price);
}
Создание форматтера выполняется один раз, после чего экземпляр переиспользуется.
При создании объектов Intl движок JavaScript выполняет
несколько тяжёлых операций:
Особенно дорого обходятся:
Intl.DateTimeFormatIntl.NumberFormatIntl.CollatorМенее затратны:
Intl.PluralRulesIntl.ListFormatIntl.RelativeTimeFormatОднако даже они не должны создаваться без необходимости.
const formatters = {};
function getCurrencyFormatter(locale, currency) {
const key = `${locale}-${currency}`;
if (!formatters[key]) {
formatters[key] = new Intl.NumberFormat(locale, {
style: 'currency',
currency
});
}
return formatters[key];
}
Использование:
const formatter = getCurrencyFormatter('en-US', 'USD');
console.log(formatter.format(1500));
Map удобнее обычного объекта при большом количестве
ключей.
const formatterCache = new Map();
function getFormatter(locale, currency) {
const key = `${locale}:${currency}`;
if (!formatterCache.has(key)) {
formatterCache.set(
key,
new Intl.NumberFormat(locale, {
style: 'currency',
currency
})
);
}
return formatterCache.get(key);
}
Преимущества:
Проблема появляется, когда объект настроек содержит множество параметров.
Плохой вариант:
new Intl.DateTimeFormat(locale, options);
где options каждый раз создаётся заново.
Оптимизация:
const cache = new Map();
function getDateFormatter(locale, options) {
const key = JSON.stringify([locale, options]);
if (!cache.has(key)) {
cache.set(
key,
new Intl.DateTimeFormat(locale, options)
);
}
return cache.get(key);
}
Использование:
const formatter = getDateFormatter('ru-RU', {
dateStyle: 'long',
timeStyle: 'short'
});
Если набор локалей известен заранее, форматтеры лучше создавать при инициализации приложения.
const formatters = {
ru: new Intl.NumberFormat('ru-RU'),
en: new Intl.NumberFormat('en-US'),
de: new Intl.NumberFormat('de-DE')
};
Это особенно эффективно:
Одна из самых дорогих ошибок производительности.
Неэффективно:
const prices = [100, 200, 300];
const result = prices.map(price => {
return new Intl.NumberFormat('ru-RU').format(price);
});
Оптимизировано:
const formatter = new Intl.NumberFormat('ru-RU');
const result = prices.map(price => {
return formatter.format(price);
});
Intl.DateTimeFormat — один из самых тяжёлых объектов во
всём API.
Проблемный код:
messages.forEach(message => {
const formatted = new Intl.DateTimeFormat(
'ru-RU',
{
dateStyle: 'short',
timeStyle: 'short'
}
).format(message.date);
console.log(formatted);
});
Оптимизированный вариант:
const formatter = new Intl.DateTimeFormat(
'ru-RU',
{
dateStyle: 'short',
timeStyle: 'short'
}
);
messages.forEach(message => {
console.log(
formatter.format(message.date)
);
});
Разница может быть заметна даже на нескольких тысячах элементов.
Intl.Collator особенно важен при сортировке.
Неэффективно:
users.sort((a, b) => {
return a.name.localeCompare(
b.name,
'ru-RU'
);
});
localeCompare внутри может создавать временные структуры
при каждом сравнении.
Лучше использовать отдельный collator:
const collator = new Intl.Collator('ru-RU');
users.sort((a, b) => {
return collator.compare(a.name, b.name);
});
При сортировке больших массивов выигрыш становится существенным.
Для таблиц и поисковых систем использование
Intl.Collator практически обязательно.
const collator = new Intl.Collator('ru', {
sensitivity: 'base',
numeric: true
});
products.sort((a, b) => {
return collator.compare(a.title, b.title);
});
Преимущества:
Даже создание объектов конфигурации влияет на производительность.
Плохо:
function formatDate(date) {
return formatter.format(date, {
year: 'numeric',
month: 'long'
});
}
Лучше:
const options = {
year: 'numeric',
month: 'long'
};
const formatter = new Intl.DateTimeFormat(
'ru-RU',
options
);
Причины:
Типичная ошибка:
function Price({ value }) {
const formatter = new Intl.NumberFormat(
'ru-RU',
{
style: 'currency',
currency: 'RUB'
}
);
return (
<span>{formatter.format(value)}</span>
);
}
При каждом рендере создаётся новый форматтер.
Оптимизация через useMemo:
import { useMemo } from 'react';
function Price({ value }) {
const formatter = useMemo(() => {
return new Intl.NumberFormat(
'ru-RU',
{
style: 'currency',
currency: 'RUB'
}
);
}, []);
return (
<span>{formatter.format(value)}</span>
);
}
const formatter = new Intl.NumberFormat(
'ru-RU',
{
style: 'currency',
currency: 'RUB'
}
);
export default {
methods: {
format(price) {
return formatter.format(price);
}
}
};
Форматтер создаётся вне компонента и переиспользуется всеми экземплярами.
На сервере стоимость создания Intl особенно заметна
из-за большого количества запросов.
Плохой вариант:
app.get('/prices', (req, res) => {
const formatter = new Intl.NumberFormat('ru-RU');
res.send(
formatter.format(1000000)
);
});
Лучше:
const formatter = new Intl.NumberFormat('ru-RU');
app.get('/prices', (req, res) => {
res.send(
formatter.format(1000000)
);
});
Для крупных приложений удобно создавать централизованный менеджер.
class FormatterPool {
constructor() {
this.cache = new Map();
}
number(locale, options) {
const key = JSON.stringify([
'number',
locale,
options
]);
if (!this.cache.has(key)) {
this.cache.set(
key,
new Intl.NumberFormat(
locale,
options
)
);
}
return this.cache.get(key);
}
date(locale, options) {
const key = JSON.stringify([
'date',
locale,
options
]);
if (!this.cache.has(key)) {
this.cache.set(
key,
new Intl.DateTimeFormat(
locale,
options
)
);
}
return this.cache.get(key);
}
}
Использование:
const pool = new FormatterPool();
const formatter = pool.number(
'ru-RU',
{
style: 'currency',
currency: 'RUB'
}
);
Бесконтрольное кэширование может привести к утечкам памяти.
Проблемный сценарий:
getFormatter(locale, dynamicOptions);
где dynamicOptions постоянно меняются.
Решения:
Если конфигурации представлены объектами:
const cache = new WeakMap();
function getFormatter(options) {
if (!cache.has(options)) {
cache.set(
options,
new Intl.NumberFormat(
'ru-RU',
options
)
);
}
return cache.get(options);
}
Преимущество WeakMap:
При обработке больших массивов важно минимизировать:
Оптимизированный пример:
const formatter = new Intl.NumberFormat('ru-RU');
const result = new Array(data.length);
for (let i = 0; i < data.length; i++) {
result[i] = formatter.format(
data[i]
);
}
Такой подход быстрее:
map;forEach;Иногда форматтеры нужны не всегда.
let formatter = null;
function format(value) {
if (!formatter) {
formatter = new Intl.NumberFormat(
'ru-RU'
);
}
return formatter.format(value);
}
Подход полезен:
Избыточное количество локалей влияет на:
Некоторые библиотеки включают десятки языков автоматически.
Оптимизация:
В Node.js существует два режима ICU:
Содержит ограниченный набор локалей.
Преимущества:
Недостатки:
Поддерживает все локали.
Преимущества:
Недостатки:
Проверка поддержки:
console.log(
Intl.DateTimeFormat.supportedLocalesOf([
'ru',
'de',
'fr',
'zh'
])
);
Для анализа используется performance.now().
const start = performance.now();
for (let i = 0; i < 10000; i++) {
formatter.format(i);
}
const end = performance.now();
console.log(end - start);
Пример бенчмарка:
const numbers = Array.from(
{ length: 100000 },
(_, i) => i
);
console.time('without-cache');
numbers.forEach(n => {
new Intl.NumberFormat('ru-RU')
.format(n);
});
console.timeEnd('without-cache');
const formatter =
new Intl.NumberFormat('ru-RU');
console.time('with-cache');
numbers.forEach(n => {
formatter.format(n);
});
console.timeEnd('with-cache');
Разница может достигать десятков раз.
Не всегда требуется форматировать значения повторно.
Плохо:
render();
render();
render();
если внутри каждый раз выполняется форматирование.
Лучше кэшировать уже готовые строки:
const formattedPrice =
formatter.format(price);
Эффективная стратегия:
function memoizeFormat(formatter) {
const cache = new Map();
return value => {
if (!cache.has(value)) {
cache.set(
value,
formatter.format(value)
);
}
return cache.get(value);
};
}
Использование:
const formatter =
new Intl.NumberFormat('ru-RU');
const format =
memoizeFormat(formatter);
console.log(format(1000));
При тяжёлой обработке данных форматирование можно переносить в
Web Worker.
Полезно для:
Главный поток остаётся отзывчивым.
При серверном рендеринге важно использовать одинаковые локали на сервере и клиенте.
Проблема:
Server: 1 234,56 ₽
Client: ₽1,234.56
Это приводит к ошибкам гидратации.
Решение:
Частое создание объектов Intl увеличивает:
Особенно это заметно:
Intl внутри циклов;JSON.stringify в горячих участках.Intl.Collator для сортировки;class IntlService {
constructor(locale) {
this.locale = locale;
this.number =
new Intl.NumberFormat(locale);
this.currency =
new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
});
this.date =
new Intl.DateTimeFormat(locale, {
dateStyle: 'medium'
});
this.collator =
new Intl.Collator(locale);
}
}
Использование:
const intl = new IntlService('ru-RU');
intl.currency.format(1500);
intl.date.format(new Date());
Подобный подход: