Оптимизация рендеринга

Одним из наиболее затратных этапов в экосистеме FormatJS является разбор ICU-сообщений и их компиляция в функции форматирования. Особенно это заметно при динамической генерации сообщений или частом пересоздании объектов сообщений в React-приложениях.

Ключевая оптимизация заключается в разделении этапов парсинга и выполнения. Сообщения должны быть статичными относительно жизненного цикла приложения, чтобы исключить повторную компиляцию.

import { defineMessages } from 'react-intl';

const messages = defineMessages({
  title: {
    id: 'page.title',
    defaultMessage: 'Добро пожаловать, {name}'
  }
});

Использование defineMessages позволяет извлечь сообщения на этапе сборки, что предотвращает повторный runtime-парсинг ICU-деревьев.

Дополнительное ускорение достигается через предкомпиляцию сообщений с помощью CLI-инструментов FormatJS:

formatjs compile src/messages src/compiled-messages

Это переносит вычислительную нагрузку из браузера в build-step.


Минимизация повторных рендеров в React-интеграции

При использовании React с FormatJS основная проблема производительности часто связана не с самой локализацией, а с повторным рендерингом компонентов, содержащих intl.formatMessage.

Каждый ререндер вызывает повторный вызов форматирования, что становится критичным при списках или таблицах.

Оптимизация строится на трех уровнях:

  1. мемоизация компонентов;
  2. стабилизация входных параметров;
  3. изоляция форматирования.
import React, { memo } from 'react';
import { useIntl } from 'react-intl';

const UserCard = memo(function UserCard({ user }) {
  const intl = useIntl();

  const label = intl.formatMessage(
    { id: 'user.label' },
    { name: user.name }
  );

  return <div>{label}</div>;
});

Использование React.memo предотвращает повторный рендер при неизменных props, снижая нагрузку на форматирование.


Стабилизация объектов сообщений и параметров

Частая ошибка — создание новых объектов сообщений внутри render-функции. Это приводит к тому, что FormatJS воспринимает сообщение как новое и выполняет повторный парсинг.

Проблемный паттерн:

intl.formatMessage(
  { id: 'order.total' },
  { value: total }
);

Оптимизированный подход:

const ORDER_TOTAL = {
  id: 'order.total'
};

intl.formatMessage(ORDER_TOTAL, { value: total });

Стабильные ссылки позволяют движку переиспользовать кэшированные структуры.


Кэширование Intl-форматтеров

FormatJS использует нативные API браузера (Intl.NumberFormat, Intl.DateTimeFormat). Их создание является дорогой операцией.

Основная стратегия оптимизации — повторное использование экземпляров форматтеров.

const numberFormatter = new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'RUB'
});

numberFormatter.format(12345);

В React-приложениях такие экземпляры должны создаваться вне компонентов или мемоизироваться через useMemo.

const formatter = useMemo(
  () =>
    new Intl.DateTimeFormat('ru-RU', {
      dateStyle: 'long'
    }),
  []
);

Снижение затрат на Provider и Context

IntlProvider в экосистеме FormatJS использует React Context. Любое изменение props провайдера приводит к повторному рендеру всего поддерева.

Основная ошибка — передача новых объектов messages на каждом рендере верхнего уровня приложения.

Антипаттерн:

<IntlProvider messages={{ hello: 'Привет' }}>

Каждое обновление создаёт новый объект и вызывает cascade-render.

Оптимизация заключается в вынесении сообщений за пределы компонента:

const messages = {
  hello: 'Привет'
};

<IntlProvider messages={messages}>

При необходимости динамической загрузки применяется кэширование по локали.


Lazy-loading переводов и code splitting

При большом количестве локалей загрузка всех переводов одновременно становится узким местом.

Стратегия оптимизации — ленивое подключение сообщений:

const loadMessages = async (locale) => {
  switch (locale) {
    case 'ru':
      return import('./lang/ru.json');
    case 'en':
      return import('./lang/en.json');
  }
};

Интеграция с динамическим импортом позволяет уменьшить initial bundle size и ускорить TTI.


Оптимизация ICU-выражений

Сложные ICU-конструкции (pluralization, select, nested rules) увеличивают стоимость парсинга и выполнения.

Пример тяжёлого выражения:

{
  "id": "cart.items",
  "defaultMessage": "{count, plural, one {# товар} few {# товара} many {# товаров} other {# товара}}"
}

Оптимизация достигается через:

  • упрощение вложенности;
  • предварительное вычисление логики вне шаблона;
  • разделение сообщений на несколько уровней.
const label = count === 1 ? 'товар' : 'товаров';

Снижение нагрузки при форматировании списков

При отображении больших списков основная проблема — повторное форматирование одинаковых значений.

Решение — мемоизация результатов форматирования:

const cache = new Map();

function formatCached(intl, id, values) {
  const key = id + JSON.stringify(values);

  if (cache.has(key)) return cache.get(key);

  const result = intl.formatMessage({ id }, values);
  cache.set(key, result);

  return result;
}

Такой подход уменьшает количество вызовов ICU-движка при повторяющихся данных.


Оптимизация работы с датами и временем

Форматирование дат является одной из самых дорогих операций в Intl.DateTimeFormat.

При высокой частоте обновлений (например, таймеры или ленты событий) применяется стратегия редкого пересчёта.

const now = Date.now();

if (now - lastUpdate > 60000) {
  formattedDate = formatter.format(now);
  lastUpdate = now;
}

Также используется разделение «сырых» и «отформатированных» данных, чтобы не вызывать форматирование на каждом рендере.


Изоляция форматирования от UI-логики

Одна из ключевых проблем производительности — смешивание форматирования и представления.

Оптимизированная архитектура предполагает выделение слоя подготовки данных:

function prepareUserView(user, intl) {
  return {
    name: user.name,
    balance: intl.formatNumber(user.balance),
    createdAt: intl.formatDate(user.createdAt)
  };
}

UI-компоненты получают уже готовые строки, избегая лишних вызовов intl во время рендера.


Управление количеством активных локалей

Каждая локаль увеличивает размер бандла и нагрузку на память при переключении.

Оптимизация включает:

  • ограничение активных языков;
  • выгрузку неиспользуемых локалей;
  • хранение только текущей локали в памяти.
const localeCache = new Map();

async function getMessages(locale) {
  if (!localeCache.has(locale)) {
    const messages = await import(`./lang/${locale}.json`);
    localeCache.set(locale, messages);
  }

  return localeCache.get(locale);
}

Снижение стоимости селекторов и context-based intl доступа

Каждый вызов useIntl() внутри множества компонентов может приводить к лишним подпискам на context.

Оптимизация заключается в выносе форматтера на уровень выше и передаче через props, когда компонент является глубоко вложенным и часто перерисовывается без изменения локали.

function Parent() {
  const intl = useIntl();

  return <Child format={intl.formatMessage} />;
}

Это уменьшает количество зависимостей от React Context и снижает вероятность каскадных обновлений.


Стабилизация ключей сообщений при масштабировании

При больших системах локализации ключи сообщений часто генерируются динамически, что приводит к невозможности кэширования.

Нормализованный подход требует строгой схемы идентификаторов:

module.section.element.state

Пример:

user.profile.button.save

Стабильность идентификаторов напрямую влияет на эффективность кэширования в FormatJS runtime и build-time оптимизации.