Серверный рендеринг

Серверный рендеринг в Globalize требует предварительной подготовки среды локализации, поскольку библиотека опирается на CLDR-данные и не может функционировать без явной загрузки ресурсов. В отличие от клиентского окружения, где данные часто подгружаются динамически и кэшируются браузером, серверная среда требует строгого контроля над инициализацией, повторным использованием экземпляров и изоляцией локалей между запросами.

В серверной среде отсутствуют встроенные данные локализации, поэтому первым шагом становится явная регистрация CLDR-данных. Globalize не поставляется с готовыми локалями и требует загрузки минимального набора JSON-файлов:

  • numbers.json
  • ca-gregorian.json
  • timeZoneNames.json (при необходимости)
  • currencies.json (если используются валюты)
  • likelySubtags.json

Инициализация выполняется один раз при старте процесса:

import Globalize from "globalize";

import likelySubtags from "cldr-data/supplemental/likelySubtags.json";
import numbers from "cldr-data/main/ru/numbers.json";
import caGregorian from "cldr-data/main/ru/ca-gregorian.json";
import timeZoneNames from "cldr-data/main/ru/timeZoneNames.json";
import currencies from "cldr-data/main/ru/currencies.json";

Globalize.load(likelySubtags);
Globalize.load(numbers);
Globalize.load(caGregorian);
Globalize.load(timeZoneNames);
Globalize.load(currencies);

Globalize.locale("ru");

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

Архитектура экземпляров локали

Серверный рендеринг требует строгого разделения локализованных контекстов. Globalize не хранит состояние в экземплярах в классическом смысле, однако привязка locale() влияет на глобальное состояние модуля. Это делает небезопасным использование единственного глобального локального контекста при обработке параллельных запросов.

Типовой подход заключается в создании фабрики форматтеров:

import Globalize from "globalize";

const getFormatter = (locale) => {
  const g = new Globalize(locale);

  return {
    number: g.numberFormatter(),
    date: g.dateFormatter(),
    currency: g.currencyFormatter("USD")
  };
};

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

const formattersCache = new Map();

function getFormatters(locale) {
  if (formattersCache.has(locale)) {
    return formattersCache.get(locale);
  }

  Globalize.locale(locale);

  const formatters = {
    number: Globalize.numberFormatter(),
    date: Globalize.dateFormatter({ datetime: "medium" }),
    currency: Globalize.currencyFormatter("EUR")
  };

  formattersCache.set(locale, formatters);
  return formatters;
}

Такой подход минимизирует повторные вычисления и снижает накладные расходы при SSR.

Проблема конкурентного доступа

Node.js выполняет запросы асинхронно, и глобальное состояние Globalize.locale() может быть перезаписано между асинхронными операциями. Это приводит к потенциальным race condition, когда один запрос может получить локаль другого.

Для устранения этой проблемы используется изоляция через контекст запроса:

function renderRequest(req) {
  const locale = req.headers["accept-language"]?.split(",")[0] || "en";

  const globalize = new Globalize(locale);

  const formatNumber = globalize.numberFormatter();
  const formatDate = globalize.dateFormatter();

  return {
    price: formatNumber(12345.67),
    date: formatDate(new Date())
  };
}

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

Предзагрузка CLDR и оптимизация памяти

CLDR-данные являются значительными по размеру, и их загрузка в SSR-окружении требует аккуратного управления памятью. Практика загрузки всех локалей одновременно часто приводит к избыточному потреблению RAM.

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

function loadLocaleData(locale) {
  const basePath = `cldr-data/main/${locale}`;

  Globalize.load(
    require(`${basePath}/numbers.json`),
    require(`${basePath}/ca-gregorian.json`)
  );
}

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

Форматирование дат в SSR

Форматирование дат требует учёта временной зоны сервера. Globalize использует CLDR, но не управляет системным временем напрямую. Поэтому необходимо нормализовать входные данные:

const g = new Globalize("ru");

const formatDate = g.dateFormatter({
  datetime: "long"
});

const timestamp = Date.now();
const result = formatDate(new Date(timestamp));

При мульти-региональных приложениях необходимо дополнительно учитывать IANA time zone, так как Globalize сам по себе не выполняет полноценную работу с часовыми поясами.

Валютное форматирование на сервере

Форматирование валюты в SSR часто используется для предварительного рендеринга цен:

const g = new Globalize("ru");

const formatCurrency = g.currencyFormatter("RUB", {
  style: "symbol"
});

const price = formatCurrency(1999.99);

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

Гидрация и согласованность вывода

Одной из критических проблем SSR является расхождение между серверным и клиентским форматированием. Globalize должен использовать идентичные данные CLDR и одинаковую локаль на обеих сторонах.

Несоответствие может возникнуть из-за:

  • различий версий CLDR
  • различий в порядке загрузки данных
  • изменения локали между сервером и клиентом
  • отсутствия time zone синхронизации

Для предотвращения таких ситуаций применяется сериализация локали:

const state = {
  locale: "ru",
  currency: "RUB"
};

const html = `
  <script>
    window.__LOCALE_STATE__ = ${JSON.stringify(state)};
  </script>
`;

На клиенте Globalize инициализируется на основе переданного состояния.

Изоляция сборки и Webpack

При использовании сборщиков важно контролировать включение CLDR-данных в бандл. Неправильная конфигурация приводит к попаданию всех локалей в серверный пакет.

Рекомендуется разделять серверную и клиентскую сборку:

externals: {
  "cldr-data": "commonjs cldr-data"
}

Также важно избегать дублирования загрузки JSON-файлов при SSR-рендере модулей React или Vue.

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

Создание форматтеров является относительно дорогой операцией. Поэтому серверные приложения используют кэширование по типу:

const cache = new Map();

function getNumberFormatter(locale, options) {
  const key = `${locale}-${JSON.stringify(options)}`;

  if (!cache.has(key)) {
    const g = new Globalize(locale);
    cache.set(key, g.numberFormatter(options));
  }

  return cache.get(key);
}

Такой подход уменьшает нагрузку при высокой частоте запросов.

Потокобезопасность и ограничения

Globalize не является потокобезопасной библиотекой в контексте изменения глобальной локали. При использовании Globalize.locale() в SSR необходимо избегать параллельных модификаций глобального состояния.

Поэтому предпочтение отдаётся либо:

  • локальным экземплярам форматтеров
  • либо кэшированным функциям без изменения глобальной локали

Работа с несколькими локалями в одном процессе

Мульти-язычные приложения требуют поддержки нескольких локалей одновременно. Это достигается через предварительную загрузку и разделение кэша:

const locales = ["ru", "en", "de"];

locales.forEach(loadLocaleData);

const formatterCache = new Map();

function get(locale) {
  if (!formatterCache.has(locale)) {
    const g = new Globalize(locale);

    formatterCache.set(locale, {
      number: g.numberFormatter(),
      date: g.dateFormatter(),
      currency: g.currencyFormatter("USD")
    });
  }

  return formatterCache.get(locale);
}

Такой подход позволяет обслуживать разные регионы без повторной инициализации CLDR.

Производительность в условиях SSR

Основные факторы, влияющие на производительность:

  • время загрузки CLDR при старте
  • количество создаваемых форматтеров
  • частота переключения локалей
  • сериализация состояния между сервером и клиентом

Наиболее затратной операцией остаётся инициализация форматтеров, поэтому их кэширование является ключевым элементом оптимизации.

Дополнительно ускорение достигается за счёт предварительного прогрева:

function warmup() {
  const g = new Globalize("ru");

  g.numberFormatter();
  g.dateFormatter();
  g.currencyFormatter("RUB");
}

Интеграция с SSR-фреймворками

В приложениях на Express или Fastify Globalize обычно инициализируется на уровне middleware:

app.use((req, res, next) => {
  req.locale = req.headers["accept-language"] || "en";
  req.globalize = new Globalize(req.locale);
  next();
});

Далее форматирование выполняется через объект запроса, исключая глобальные конфликты.

При использовании React SSR важно, чтобы форматирование происходило до сериализации HTML, иначе возникает риск рассинхронизации с клиентской гидрацией.