В основе экосистемы FormatJS лежит работа с ICU MessageFormat —
форматом строк, содержащих условия, множественные формы, интерполяцию и
вложенные конструкции. Парсинг таких сообщений является одним из
наиболее затратных этапов, особенно в рантайме, поэтому архитектура
библиотеки изначально ориентирована на минимизацию повторных вычислений
и перенос тяжёлых операций в фазу сборки.
Парсинг сообщения включает преобразование строки в абстрактное
синтаксическое дерево (AST), которое затем используется для
форматирования. Этот процесс выполняется парсером
@formatjs/icu-messageformat-parser.
Ключевая особенность заключается в том, что:
разбор строки ICU → AST → кешируемая структура →
форматирование
чем меньше раз происходит первый шаг, тем выше общая
производительность приложения.
Стоимость парсинга ICU-строк
ICU-сообщения поддерживают сложную грамматику:
- plural rules
- select expressions
- вложенные блоки
- аргументы с форматированием дат, чисел, валют
- escape-последовательности
- переменные интерполяции
Каждая из этих конструкций требует синтаксического анализа с
построением дерева.
Пример сообщения:
"You have {count, plural, one {1 item} other {# items}}"
Парсер должен:
- определить переменную
count
- распознать тип
plural
- разобрать правила
one и other
- построить дерево условий
- подготовить структуру для быстрого выбора ветки
Стоимость такого анализа растёт пропорционально:
- количеству сообщений
- глубине вложенности
- частоте повторного парсинга
Проблема повторного парсинга
Наивное использование FormatJS приводит к ситуации, когда одно и то
же сообщение парсится многократно:
- при каждом рендере компонента
- при каждом вызове
formatMessage
- при изменении состояния React-дерева
- при пересоздании
intl контекста
Это создаёт узкое место, поскольку парсер ICU не является дешёвой
операцией.
Основная цель оптимизации — гарантировать, что парсинг выполняется
один раз на сообщение.
Кеширование AST как
основная стратегия
В FormatJS применяется многоуровневое кеширование:
1. Кеш на уровне сообщений
Сообщение после парсинга превращается в AST и сохраняется в кеше:
- ключ: исходная строка ICU
- значение: AST-дерево
При повторном вызове используется уже разобранная структура.
2. Локальный кеш IntlProvider
В IntlProvider сообщения компилируются один раз при
инициализации:
- сообщения локали превращаются в набор AST
- кеш привязан к текущей локали
- переключение локали инвалидирует кеш
Функция форматирования может использовать мемоизацию по:
- id сообщения
- значения аргументов
- локали
Предкомпиляция сообщений
Наиболее эффективная стратегия — исключение парсинга из рантайма.
FormatJS предоставляет инструменты сборки:
@formatjs/cli
babel-plugin-formatjs
Они выполняют:
- извлечение сообщений из кода
- нормализацию ICU строк
- парсинг в AST на этапе сборки
- генерацию JSON-структур
В результате в продакшене:
- отсутствует парсинг строк
- используется уже готовое AST
- остаётся только быстрый проход по дереву
Разделение compile-time и
runtime
Производительность достигается за счёт чёткого разделения этапов:
Compile-time
- извлечение сообщений
- парсинг ICU
- валидация синтаксиса
- генерация структур данных
Runtime
- выбор сообщения по id
- подстановка значений
- выбор ветки plural/select
- форматирование чисел и дат
Чем больше логики перенесено в compile-time, тем меньше нагрузка в
браузере.
Влияние сложности ICU на
скорость
Не все сообщения одинаково затратны.
Дешёвые случаи:
"Hello {name}"
- один уровень AST
- минимальный парсинг
Средняя сложность:
"{count, plural, one {# item} other {# items}}"
Высокая сложность:
"{gender, select, male {He} female {She} other {They}} has {count, plural, ...}"
- несколько уровней select
- вложенные plural
- комбинированные условия
Рост сложности увеличивает:
- время парсинга
- размер AST
- стоимость обхода дерева при форматировании
Оптимизация структуры
сообщений
Производительность напрямую зависит от структуры ICU-строк.
Минимизация вложенности
Глубокие вложенные конструкции ухудшают производительность:
select → plural → select → plural
Каждый уровень добавляет:
- дополнительный проход по дереву
- увеличение числа проверок условий
Разбиение сообщений
Сложные конструкции выгоднее разбивать:
- несколько простых сообщений вместо одного сложного
- вычисление логики вне ICU
Ленивая загрузка локалей
Большие приложения часто содержат:
- десятки языков
- тысячи сообщений
Загрузка всех локалей сразу приводит к:
- росту времени парсинга при старте
- увеличению памяти
- замедлению гидратации SSR
Оптимизация:
- динамическая загрузка локалей
- код-сплиттинг по языкам
- кеширование уже загруженных AST
SSR и гидратация
В серверном рендеринге возникает дополнительная проблема:
- сообщения уже могут быть отформатированы на сервере
- на клиенте происходит повторная инициализация intl
Если AST не сериализован:
- происходит повторный парсинг на клиенте
- увеличивается время гидратации
Оптимальный подход:
- сериализация готовых сообщений
- передача AST в клиент
- восстановление без повторного парсинга
Форматирование
чисел и дат как скрытая стоимость
Хотя основная нагрузка связана с парсингом ICU, дополнительную
стоимость дают:
Intl.NumberFormat
Intl.DateTimeFormat
FormatJS использует их поверх AST, поэтому оптимизации включают:
- кеширование форматтеров
- переиспользование экземпляров
Intl
- ограничение количества локализованных форматов
Влияние размера bundle
Парсинг тесно связан с размером библиотеки:
- чем больше ICU parser, тем выше время загрузки
- tree-shaking снижает стоимость
- исключение dev-валидаций ускоряет прод
Особенно критично:
- удаление debug-режима
- отключение runtime-валидации ICU строк
Типичные узкие места
На практике деградация производительности возникает в следующих
сценариях:
- повторная инициализация
IntlProvider
- отсутствие кеширования сообщений
- динамическая генерация ICU строк
- частые переключения локали
- отсутствие предкомпиляции
- глубокие select/plural вложенности
Стратегии снижения
стоимости парсинга
Основные подходы:
- перенос парсинга в build-step
- кеширование AST на уровне приложения
- минимизация ICU-сложности
- разделение сообщений на простые единицы
- ленивое подключение локалей
- переиспользование Intl-форматтеров
- исключение повторной инициализации контекста
Поведение при
масштабировании
При увеличении количества сообщений:
- линейно растёт стоимость первичного парсинга
- кеширование снижает повторные затраты до O(1)
- основная нагрузка смещается в форматирование
При правильно организованной архитектуре:
- парсинг становится одноразовой операцией
- runtime сводится к обходу AST
- узкие места смещаются к UI-рендерингу, а не к i18n-слою