Интернационализация в JavaScript связана с интенсивным использованием
объектов семейства Intl:
Intl.NumberFormatIntl.DateTimeFormatIntl.RelativeTimeFormatIntl.PluralRulesIntl.ListFormatСоздание экземпляров этих объектов является сравнительно дорогой операцией. При большом количестве повторяющихся форматирований — особенно в React-приложениях — постоянное создание новых formatter-объектов приводит к:
FormatJS решает эту проблему через систему мемоизации.
Мемоизация — это механизм кеширования результатов функции на основании входных параметров.
Если функция вызывается повторно с теми же аргументами, вместо нового вычисления возвращается ранее сохранённый результат.
Упрощённая схема:
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
В контексте FormatJS кешируются:
Без кеширования:
function formatPrice(value) {
const formatter = new Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB'
});
return formatter.format(value);
}
При каждом вызове:
formatPrice(100);
formatPrice(200);
formatPrice(300);
создаются новые экземпляры Intl.NumberFormat.
В больших приложениях это становится серьёзной проблемой.
FormatJS использует внутренний кеш formatter-объектов.
Пример:
import {createIntl, createIntlCache} from '@formatjs/intl';
const cache = createIntlCache();
const intl = createIntl(
{
locale: 'ru',
messages: {}
},
cache
);
createIntlCache() создаёт общий кеш для всех
formatter-объектов.
FormatJS кеширует:
| Тип formatter | Класс Intl |
|---|---|
| Форматирование чисел | Intl.NumberFormat |
| Форматирование дат | Intl.DateTimeFormat |
| Относительное время | Intl.RelativeTimeFormat |
| Плюрализация | Intl.PluralRules |
| Списки | Intl.ListFormat |
| Сообщения ICU | Internal message cache |
createIntlCache создаёт централизованный объект хранения
formatter-экземпляров.
import {createIntlCache} from '@formatjs/intl';
const cache = createIntlCache();
Структура кеша скрыта внутри библиотеки.
Фактически объект содержит несколько внутренних Map.
import {createIntl, createIntlCache} from '@formatjs/intl';
const cache = createIntlCache();
const intl = createIntl(
{
locale: 'en',
messages: {}
},
cache
);
Теперь при вызовах:
intl.formatNumber(1000);
intl.formatNumber(2000);
intl.formatNumber(3000);
FormatJS повторно использует один и тот же
Intl.NumberFormat.
FormatJS строит ключ кеша из:
Например:
intl.formatNumber(1000, {
style: 'currency',
currency: 'USD'
});
создаёт ключ примерно такого вида:
en-number-style:currency-currency:USD
При повторном вызове:
intl.formatNumber(2000, {
style: 'currency',
currency: 'USD'
});
используется уже существующий formatter.
Кеш можно не передавать:
const intl = createIntl({
locale: 'en',
messages: {}
});
Однако в этом случае:
В production-приложениях кеш почти всегда обязателен.
Один кеш может использоваться несколькими экземплярами
intl.
const cache = createIntlCache();
const intlEn = createIntl(
{
locale: 'en',
messages: {}
},
cache
);
const intlRu = createIntl(
{
locale: 'ru',
messages: {}
},
cache
);
FormatJS автоматически разделяет formatter-объекты по локалям.
ICU-сообщения требуют:
Сообщение:
Hello, {name}!
не используется напрямую.
Сначала оно преобразуется во внутреннее представление.
intl.formatMessage(
{
id: 'welcome',
defaultMessage: 'Hello, {name}!'
},
{
name: 'John'
}
);
FormatJS сохраняет результат компиляции сообщения в кеше.
При повторном вызове:
intl.formatMessage(
{
id: 'welcome',
defaultMessage: 'Hello, {name}!'
},
{
name: 'Kate'
}
);
повторный парсинг уже не выполняется.
В react-intl мемоизация играет особенно важную роль.
Причина — частые re-render’ы компонентов.
function Price({value}) {
const formatter = new Intl.NumberFormat('en', {
style: 'currency',
currency: 'USD'
});
return <span>{formatter.format(value)}</span>;
}
При каждом рендере:
IntlProvider внутри использует memoized intl-object.
<IntlProvider locale="en" messages={messages}>
<App />
</IntlProvider>
Библиотека переиспользует:
Для ручного управления:
import {
createIntl,
createIntlCache,
RawIntlProvider
} from 'react-intl';
const cache = createIntlCache();
const intl = createIntl(
{
locale: 'en',
messages
},
cache
);
function App() {
return (
<RawIntlProvider value={intl}>
<Main />
</RawIntlProvider>
);
}
Такой подход часто используется:
На сервере:
Мемоизация существенно уменьшает количество объектов.
Пример:
const cache = createIntlCache();
export function createRequestIntl(locale, messages) {
return createIntl(
{
locale,
messages
},
cache
);
}
Общий кеш может использоваться между запросами.
Нельзя сохранять данные пользователя внутри formatter options.
Плохой пример:
intl.formatDate(date, {
timeZone: user.timezone
});
Если options сильно различаются:
FormatJS не реализует автоматическую очистку кеша.
Это означает:
intl.formatNumber(value, {
minimumFractionDigits: random()
});
Каждый вызов создаёт новый formatter.
Кеш становится бесполезным.
const currencyOptions = {
style: 'currency',
currency: 'USD'
};
intl.formatNumber(price, currencyOptions);
intl.formatNumber(price, {
style: 'currency',
currency: dynamicCurrency(),
minimumFractionDigits: random()
});
Даже одинаковые по содержимому объекты могут вызывать лишние вычисления при неправильной архитектуре.
Особенно это важно в React.
<Component
options={{
style: 'currency',
currency: 'USD'
}}
/>
Новый объект создаётся на каждом рендере.
const options = useMemo(
() => ({
style: 'currency',
currency: 'USD'
}),
[]
);
useIntl возвращает стабильный объект
intl.
const intl = useIntl();
Однако formatter options всё ещё требуют аккуратного обращения.
FormatJS кеширует также Intl.ListFormat.
intl.formatList(['JavaScript', 'TypeScript', 'Rust']);
Formatter создаётся один раз для конкретной локали и options.
intl.formatRelativeTime(-1, 'day');
Используется кешированный Intl.RelativeTimeFormat.
intl.formatPlural(5);
Внутри используется Intl.PluralRules.
Эти объекты тоже являются дорогостоящими.
Типичный сценарий:
Без мемоизации:
console.time('without-cache');
for (let i = 0; i < 100000; i++) {
new Intl.NumberFormat('en').format(i);
}
console.timeEnd('without-cache');
С мемоизацией:
console.time('with-cache');
const formatter = new Intl.NumberFormat('en');
for (let i = 0; i < 100000; i++) {
formatter.format(i);
}
console.timeEnd('with-cache');
Разница может составлять десятки раз.
Главная выгода мемоизации — уменьшение краткоживущих объектов.
Без кеша:
С кешем:
Правильно:
const cache = createIntlCache();
Неправильно:
function render() {
const cache = createIntlCache();
}
Часто кеш создаётся как singleton.
// intl-cache.js
import {createIntlCache} from '@formatjs/intl';
export const intlCache = createIntlCache();
Иногда используют отдельные кеши:
Без общего кеша тесты могут работать медленнее.
Пример:
const cache = createIntlCache();
beforeEach(() => {
intl = createIntl(
{
locale: 'en',
messages: {}
},
cache
);
});
Intl.DateTimeFormat особенно дорог в создании.
intl.formatDate(date, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
FormatJS кеширует formatter по:
Проблемный сценарий:
intl.formatDate(date, {
timeZone: userTimeZone
});
Если timezone уникален для каждого пользователя:
Лучший способ контроля памяти — стандартизация formatter-конфигураций.
const SHORT_DATE = {
year: 'numeric',
month: 'short',
day: 'numeric'
};
Внутри библиотеки используются специализированные memoization utility-функции.
Они:
Map;Пример:
const cache = {};
Недостатки:
Map предоставляет:
В React concurrent mode количество рендеров возрастает.
Без мемоизации:
FormatJS минимизирует эти издержки.
Во время hydration React повторно выполняет рендер компонентов.
Кеш formatter-объектов позволяет избежать повторного создания Intl-инстансов.
FormatJS компилирует ICU-сообщения в AST.
Пример:
{count, plural,
one {# item}
other {# items}
}
Парсинг таких шаблонов является вычислительно затратным.
Для максимальной производительности используют предварительную компиляцию сообщений.
Пакет:
@formatjs/cli
позволяет:
Плюсы:
Минусы:
Плюсы:
Минусы:
Симптомы плохой мемоизации:
Intl.*.В Chrome DevTools можно увидеть:
Intl.NumberFormat;Плохо:
function App() {
const intl = createIntl(...);
}
Плохо:
function App() {
const cache = createIntlCache();
}
Плохо:
intl.formatNumber(value, {
minimumFractionDigits: Math.random()
});
const cache = createIntlCache();
const intl = createIntl(
{
locale: 'en',
messages
},
cache
);