Серверный рендеринг с использованием Intl API требует строгого контроля над локалями, временем выполнения и детерминированностью вывода. Основная проблема SSR в контексте форматирования заключается в том, что один и тот же код может выполняться в разных средах — на сервере и в браузере — где доступны разные настройки локали, часового пояса и даже реализации ICU.
Ключевое требование серверного рендеринга — идентичность результата на сервере и клиенте. Любое расхождение в строках приводит к проблемам гидратации.
Intl API напрямую зависит от:
en-US, ru-RU,
de-DE)Даже незначительное отличие, например формат числа или даты, приводит к различию HTML.
Пример проблемного поведения:
1 234,56 ₽1,234.56 ₽Причина — различие локали или параметров форматирования.
Использование системной локали без явного указания недопустимо в SSR.
Нестабильный вариант:
new Intl.NumberFormat().format(1234.56);
Стабильный вариант:
new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(1234.56);
Явное указание локали устраняет зависимость от окружения и обеспечивает воспроизводимость результата.
Дата — один из наиболее частых источников расхождений между сервером и клиентом.
Основная проблема — различие часовых поясов:
new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date());
Без фиксации timeZone результат может различаться:
new Intl.DateTimeFormat('ru-RU', {
timeZone: 'UTC'
}).format(new Date());
Использование фиксированной временной зоны в SSR обеспечивает предсказуемый вывод и предотвращает расхождение HTML.
В серверной среде локаль обычно извлекается из HTTP-заголовка:
Accept-Language: ru-RU,ru;q=0.9,en;q=0.8
Типичный подход — выбор первой подходящей локали:
const locale = request.headers['accept-language']?.split(',')[0] || 'en-US';
Эта локаль используется во всех форматерах на протяжении одного запроса, что обеспечивает консистентность вывода.
Создание экземпляров Intl дорогостоящее по сравнению с
использованием готовых объектов. В SSR это критично при высокой
нагрузке.
Плохая практика — создание форматера на каждое значение:
formatPrice(price) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(price);
}
Оптимизированный вариант — кэширование:
const formatters = new Map();
function getCurrencyFormatter(locale) {
if (!formatters.has(locale)) {
formatters.set(
locale,
new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'RUB'
})
);
}
return formatters.get(locale);
}
Такой подход снижает нагрузку на GC и ускоряет рендеринг.
В SSR критично избегать утечек состояния между запросами. Кэш форматеров должен быть привязан к локали и параметрам, но не содержать пользовательских данных.
Недопустимо:
Корректный ключ кэша:
locale + currency + optionsHash
Фреймворки с гидратацией (React, Vue, Svelte) требуют, чтобы HTML сервера совпадал с результатом первого рендера клиента.
Типичные источники ошибок:
timeZoneDate.now() во время рендераДаже разный пробел или порядок символов ломает гидратацию.
Intl.RelativeTimeFormat часто используется для
SSR-комментариев, лент и уведомлений.
const rtf = new Intl.RelativeTimeFormat('ru-RU', {
numeric: 'auto'
});
rtf.format(-1, 'day');
Важно фиксировать единицу измерения и локаль, иначе возможны различия:
При разных настройках numeric результат меняется, что
может привести к несоответствию SSR и клиента.
Intl.ListFormat и Intl.Collator также
требуют стабильной конфигурации.
new Intl.ListFormat('ru-RU', {
style: 'long',
type: 'conjunction'
}).format(['яблоки', 'груши', 'сливы']);
Коллокация влияет на сортировку:
new Intl.Collator('ru-RU', {
sensitivity: 'base'
}).compare(a, b);
Если сортировка выполняется на сервере, она должна использовать тот
же locale, что и клиент.
Node.js может работать в разных режимах ICU:
Это влияет на:
В SSR это может приводить к тому, что сервер и клиент формируют разные строки даже при одинаковом коде.
В edge-окружениях (Cloudflare Workers, Vercel Edge и аналогах) Intl API может быть ограничен:
Это требует минимизации зависимости от неявного поведения и строгого указания параметров.
В устойчивой SSR-архитектуре форматеры выделяются в отдельный слой:
Пример структуры:
function createFormatters(locale) {
return {
number: new Intl.NumberFormat(locale),
date: new Intl.DateTimeFormat(locale),
currency: new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
})
};
}
Такой подход упрощает контроль и снижает вероятность расхождений.
Для предотвращения расхождений используется стратегия “server-first formatting”:
Пример передачи контекста:
{
locale: 'ru-RU',
timeZone: 'UTC',
currency: 'RUB'
}
Клиент использует те же параметры, но не пересчитывает уже отрисованный текст.
Intl операции дешевле, чем ручное форматирование, но при массовом рендеринге становятся значимыми.
Оптимизации:
Особенно критично в списках и таблицах, где тысячи элементов могут форматироваться одновременно.
Использование Date без контроля времени ведёт к
различиям между окружениями.
Проблемные случаи:
Intl форматирование должно работать только на основе заранее нормализованных временных значений.