Интернационализация редко становится главным источником деградации производительности, однако в крупных приложениях FormatJS способен создавать заметную нагрузку:
Профилирование позволяет определить:
Intl.* API на CPU;FormatJS состоит из нескольких уровней:
Intl.* API браузера или Node.js.react-intl).Каждый уровень влияет на производительность по-разному.
На практике самыми затратными являются:
| Операция | Стоимость |
|---|---|
Создание Intl.DateTimeFormat |
Высокая |
Создание Intl.NumberFormat |
Высокая |
| Парсинг ICU-сообщений | Средняя |
| React rerender | Средняя |
| Форматирование простых строк | Низкая |
| Чтение сообщений из памяти | Низкая |
Неправильные измерения встречаются чаще корректных.
Типичные ошибки:
Плохой пример:
console.time('format')
for (let i = 0; i < 10; i++) {
intl.formatMessage({
id: 'welcome'
})
}
console.timeEnd('format')
Такой тест практически бесполезен:
const iterations = 100000
console.time('format')
for (let i = 0; i < iterations; i++) {
intl.formatMessage({
id: 'welcome'
})
}
console.timeEnd('format')
Перед измерениями движок JavaScript должен оптимизировать код.
for (let i = 0; i < 10000; i++) {
intl.formatMessage({
id: 'welcome'
})
}
После прогрева запускается основной тест.
Измеряться должна только одна операция.
Плохо:
intl.formatMessage(...)
console.log(...)
updateState(...)
Хорошо:
intl.formatMessage(...)
formatMessageimport {createIntl, createIntlCache} fr om '@formatjs/intl'
const cache = createIntlCache()
const intl = createIntl({
locale: 'en',
messages: {
hello: 'Hello {name}'
}
}, cache)
const iterations = 100000
console.time('formatMessage')
for (let i = 0; i < iterations; i++) {
intl.formatMessage(
{id: 'hello'},
{name: 'John'}
)
}
console.timeEnd('formatMessage')
Внутри formatMessage происходят:
Если AST уже закэширован — часть шагов пропускается.
'Hello world'
Форматируются крайне быстро.
'Hello {name}'
Требуют интерполяции значений.
'{count, plural,
one {# item}
other {# items}
}'
Стоимость увеличивается:
'{gender, select,
male {He}
female {She}
other {They}
}'
select обычно дешевле plural, но всё равно
требует дополнительных операций.
'{count, plural,
one {{gender, select,
male {He has one item}
female {She has one item}
other {They have one item}
}}
other {{gender, select,
male {He has # items}
female {She has # items}
other {They have # items}
}}
}'
Такие шаблоны становятся существенно тяжелее.
const messages = {
simple: 'Hello',
variable: 'Hello {name}',
plural: `{count, plural,
one {# item}
other {# items}
}`
}
Бенчмарк:
function benchmark(id, values) {
const iterations = 100000
console.time(id)
for (let i = 0; i < iterations; i++) {
intl.formatMessage({id}, values)
}
console.timeEnd(id)
}
benchmark('simple')
benchmark('variable', {name: 'John'})
benchmark('plural', {count: 5})
Очень дорого:
function formatDate(date) {
const formatter = new Intl.DateTimeFormat('en')
return formatter.format(date)
}
Создание Intl.DateTimeFormat — одна из самых затратных
операций в интернационализации.
const formatter = new Intl.DateTimeFormat('en')
function formatDate(date) {
return formatter.format(date)
}
createIntlCacheimport {createIntlCache} fr om '@formatjs/intl'
const cache = createIntlCache()
Кэш хранит:
Intl.NumberFormat;Intl.DateTimeFormat;Intl.RelativeTimeFormat;Intl.PluralRules;const intl = createIntl({
locale: 'en',
messages
})
const cache = createIntlCache()
const intl = createIntl({
locale: 'en',
messages
}, cache)
Разница может достигать десятков раз при интенсивном форматировании.
performance.nowconst start = performance.now()
intl.formatMessage({
id: 'hello'
})
const end = performance.now()
console.log(end - start)
performance.now():
performance.mark('start')
intl.formatMessage({
id: 'hello'
})
performance.mark('end')
performance.measure(
'message-format',
'start',
'end'
)
После использования performance.measure данные
появляются:
Основной инструмент браузерного анализа.
Позволяет определить:
Типичный сценарий:
Обычно видны:
formatMessage;IntlMessageFormat;Intl.DateTimeFormat;react-intlЧрезмерные рендеры компонентов:
<FormattedMessage id="hello" />
Если компонент рендерится слишком часто — форматирование повторяется.
Позволяет определить:
<Profiler
id="App"
onRen der={callback}
>
<App />
</Profiler>
function callback(
id,
phase,
actualDuration
) {
console.log(id)
console.log(phase)
console.log(actualDuration)
}
<IntlProvider
locale={locale}
messages={{...messages}}
>
Каждый рендер создаёт новый объект сообщений.
const memoizedMessages = useMemo(
() => messages,
[locale]
)
IntlProviderПри изменении props:
<IntlProvider
locale={locale}
messages={messages}
>
React пересоздаёт контекст.
Это приводит к:
const intlConfig = useMemo(() => ({
locale,
messages
}), [locale])
FormatJS активно создаёт:
При большом количестве локалей это становится заметным.
Позволяет определить:
Показывает:
const cache = new Map()
function getIntl(locale) {
if (!cache.has(locale)) {
cache.set(locale, createIntl(...))
}
return cache.get(locale)
}
Если локалей много — кэш растёт бесконечно.
class LRUCache {
constructor(lim it = 10) {
this.limit = limit
this.map = new Map()
}
}
На сервере:
Особенно дорогими становятся:
intl;import {renderToString} fr om 'react-dom/server'
console.time('ssr')
renderToString(<App />)
console.timeEnd('ssr')
intl-объектовСоздание intl на каждый запрос дорого.
Возможен пул объектов:
const intlPool = new Map()
function getIntl(locale) {
if (!intlPool.has(locale)) {
intlPool.set(
locale,
createIntl(...)
)
}
return intlPool.get(locale)
}
Если ICU-парсинг выполняется в runtime:
intl.formatMessage(...)
часть времени уходит на разбор шаблонов.
FormatJS CLI умеет компилировать сообщения заранее.
Это уменьшает:
{
"hello": [
{
"type": 0,
"value": "Hello"
}
]
}
createIntl({
messages: rawMessages
})
createIntl({
messages: compiledMessages
})
node --profnode --prof app.js
Создаёт V8 profile log.
node --prof-process isolate.log
Инструмент:
Особенно полезен для SSR-приложений.
Стандарт де-факто для JavaScript-бенчмарков.
import Benchmark from 'benchmark'
const suite = new Benchmark.Suite()
suite
.add('format simple', () => {
intl.formatMessage({
id: 'simple'
})
})
.add('format plural', () => {
intl.formatMessage(
{id: 'plural'},
{count: 5}
)
})
suite
.on('cycle', event => {
console.log(String(event.target))
})
.run()
Например:
en обычно дешевле;ar сложнее;zh может использовать специфические правила;const locales = ['en', 'ar', 'ru', 'zh']
one / other
Минимальная сложность.
one / few / many / other
Больше условий.
Содержит ещё больше plural categories.
Это влияет на стоимость Intl.PluralRules.
memoexport default React.memo(Component)
Плохо:
<FormattedMessage
values={{
count
}}
/>
const values = useMemo(() => ({
count
}), [count])
Все локали в одном bundle:
import en from './en.json'
import ru from './ru.json'
import fr from './fr.json'
Увеличивают:
const messages = await import(
`./locales/${locale}.json`
)
FormatJS polyfills могут быть тяжёлыми.
Особенно:
Intl.RelativeTimeFormat;Intl.PluralRules;Intl.DisplayNames.Инструменты:
Современные браузеры уже поддерживают большую часть
Intl.
Лишние polyfills увеличивают:
if (!Intl.RelativeTimeFormat) {
await import(
'@formatjs/intl-relativetimeformat/polyfill'
)
}
Время выполнения форматирования.
Потребление памяти.
Количество операций в секунду.
Время инициализации i18n.
Стоимость React-рендера.
Типичный тяжёлый кейс:
10000 строк
×
5 колонок
×
форматирование дат
Использование:
react-window;react-virtualized.Существенно уменьшает нагрузку.
FormattedMessage<FormattedMessage id="hello" />
intl.formatMessage({
id: 'hello'
})
Imperative API обычно быстрее:
Ширина блока:
Высота:
Если значительную ширину занимает:
IntlMessageFormat;formatToParts;PluralRules;значит проблема связана с i18n.
formatToPartsintl.formatDate(date)
обычно быстрее:
intl.formatDateToParts(date)
Простые сообщения дешевле сложных ICU-шаблонов.
Плохо:
<IntlProvider>
<Page />
</IntlProvider>
внутри большого числа поддеревьев.
Performance-тесты должны входить в CI.
expect(duration).toBeLessThan(50)
Правильно:
const samples = []
Собираются десятки измерений.
GC и фоновые процессы могут создавать аномальные значения.
Производительность Intl различается:
| Движок | Особенности |
|---|---|
| V8 | Очень быстрый JIT |
| SpiderMonkey | Хорошая работа с ICU |
| JavaScriptCore | Отличается поведением GC |
createIntlCache.FormatJS редко становится узким местом в небольших приложениях.
Оптимизация особенно важна при: