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

В основе экосистемы 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
  • кеш привязан к текущей локали
  • переключение локали инвалидирует кеш

3. Мемоизация formatMessage

Функция форматирования может использовать мемоизацию по:

  • 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}}"
  • ветвление
  • правила plural

Высокая сложность:

"{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-слою