Использование встроенного Intl в JavaScript часто
воспринимается как бесплатное с точки зрения размера бандла, однако в
реальных приложениях влияние на итоговый размер сборки зависит от
архитектуры проекта, используемых полифиллов и стратегии поддержки
браузеров.
Основной источник роста bundle size связан не с самим
Intl, а с дополнительными слоями, которые подключаются
поверх него: полифиллы, библиотеки форматирования и локализационные
данные (CLDR/ICU). При неправильной организации импорта эти зависимости
способны увеличить итоговый бандл на сотни килобайт и более.
Современные движки JavaScript предоставляют нативную реализацию
Intl, включающую:
Intl.DateTimeFormatIntl.NumberFormatIntl.RelativeTimeFormatIntl.PluralRulesIntl.CollatorIntl.ListFormatIntl.SegmenterИспользование нативных API вместо библиотек вроде
moment.js или date-fns/format позволяет
устранить необходимость в тяжелых runtime-зависимостях.
Особенность заключается в том, что нативный Intl не
попадает в JavaScript bundle вообще. Однако косвенные зависимости могут
нивелировать это преимущество.
Главный фактор, влияющий на размер и поведение Intl, —
ICU (International Components for Unicode).
В Node.js существуют разные режимы сборки:
При использовании minimal ICU часть локалей недоступна, что приводит к необходимости подключения дополнительных пакетов:
full-icuicu-data-regexПолный ICU увеличивает размер бинарника Node.js, но не увеличивает client-side bundle. Однако в server-side rendering сценариях это напрямую влияет на размер деплоя.
На клиентской стороне основной источник увеличения bundle size —
полифиллы Intl. Особенно это касается старых браузеров, где
отсутствуют:
Intl.RelativeTimeFormatIntl.ListFormatIntl.SegmenterПопулярные решения:
@formatjs/intl-datetimeformat@formatjs/intl-numberformat@formatjs/intl-relativetimeformatПроблема классических подходов — импорт всего пакета локалей:
import '@formatjs/intl-datetimeformat/polyfill';
import '@formatjs/intl-datetimeformat/locale-data/full';
Импорт full приводит к включению всех локалей сразу, что
резко увеличивает размер бандла.
Оптимизация достигается за счет точечного подключения только необходимых локалей.
Пример частичной загрузки:
import '@formatjs/intl-datetimeformat/polyfill';
import '@formatjs/intl-datetimeformat/locale-data/en';
import '@formatjs/intl-datetimeformat/locale-data/ru';
Такой подход позволяет:
Критический эффект достигается при большом количестве поддерживаемых языков, где full-таблицы CLDR могут превышать 1–2 MB.
Современные бандлеры (Webpack, Vite, Rollup) способны удалять неиспользуемый код при наличии корректных ESM-экспортов.
Однако Intl-полифиллы часто содержат:
globalThis.IntlЭто делает tree-shaking частично неэффективным.
Пример проблемного импорта:
import '@formatjs/intl-numberformat/polyfill';
Даже если используется только NumberFormat для одной
локали, весь пакет может быть включён в initial bundle из-за side
effects.
Для снижения влияния применяется стратегия:
Наиболее эффективный подход к оптимизации bundle size — разделение локалей по чанкам и загрузка по требованию.
async function loadLocale(locale) {
switch (locale) {
case 'ru':
await import('@formatjs/intl-datetimeformat/locale-data/ru');
break;
case 'en':
await import('@formatjs/intl-datetimeformat/locale-data/en');
break;
}
}
Динамическая загрузка позволяет:
В SPA архитектурах это особенно эффективно при смене языка в runtime.
Основной принцип оптимизации заключается в максимальном использовании
встроенного Intl вместо сторонних библиотек.
Сравнение подходов:
| Подход | Размер | Поддержка локалей | Гибкость |
|---|---|---|---|
| moment.js | высокий | ограниченная | высокая |
| date-fns | средний | зависит от сборки | высокая |
| native Intl | нулевой в bundle | зависит от браузера | средняя |
Использование Intl.DateTimeFormat:
const formatter = new Intl.DateTimeFormat('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
formatter.format(new Date());
Такой код не увеличивает bundle size, поскольку использует встроенный API движка.
Архитектура оптимизированного приложения часто разделяет:
Пример структуры:
/src
/intl
polyfills.js
locales.js
/app
index.js
polyfills.js:
import '@formatjs/intl-datetimeformat/polyfill';
import '@formatjs/intl-numberformat/polyfill';
locales.js:
import '@formatjs/intl-datetimeformat/locale-data/ru';
import '@formatjs/intl-numberformat/locale-data/ru';
Далее этот слой подключается условно:
if (!Intl.DateTimeFormat) {
await import('./intl/polyfills');
}
Webpack требует явной настройки sideEffects:
{
"sideEffects": false
}
Если библиотека Intl-полифиллов помечена как side-effect-free, tree-shaking становится эффективнее.
Также используется code splitting:
optimization: {
splitChunks: {
chunks: 'all'
}
}
Vite по умолчанию использует ES modules и более агрессивный tree-shaking. Однако Intl-полифиллы всё равно могут попадать в initial chunk при static import.
Оптимизация достигается через:
optimizeDeps)Многие проекты сохраняют legacy зависимости:
При переходе на Intl устраняется необходимость в:
Особенно значительное уменьшение bundle size наблюдается при отказе от moment-timezone, где размер может превышать 200–400 KB gzipped.
Форматирование дат, чисел и списков часто не требуется при первом рендере приложения. Это позволяет отложить загрузку Intl-обвязки:
let dateFormatter;
async function getFormatter() {
if (!dateFormatter) {
dateFormatter = new Intl.DateTimeFormat('ru-RU');
}
return dateFormatter;
}
В более сложных случаях используется полное разделение чанков:
const formatDate = async (date) => {
const { formatter } = await import('./dateFormatter');
return formatter.format(date);
};
Частая ошибка — включение всех локалей “на будущее”.
Правильная стратегия:
Пример анти-паттерна:
import * as locales from '@formatjs/intl-datetimeformat/locale-data';
Такой импорт гарантированно увеличивает bundle size до максимального объема доступных данных.
| Стратегия | Initial bundle | Lazy loading | Поддержка локалей |
|---|---|---|---|
| full polyfill | высокий | нет | максимальная |
| partial polyfill | средний | частично | ограниченная |
| native Intl | минимальный | не требуется | зависит от окружения |
| hybrid (native + polyfill fallback) | низкий | да | контролируемая |
Наиболее устойчивый подход сочетает:
Intl APIif (!('RelativeTimeFormat' in Intl)) {
await import('@formatjs/intl-relativetimeformat/polyfill');
await import('@formatjs/intl-relativetimeformat/locale-data/ru');
}
Такая модель минимизирует initial bundle и сохраняет совместимость со старыми окружениями.
Оптимизация Intl часто блокируется не самим API, а сторонними библиотеками:
Решение заключается в унификации источника форматирования: только
Intl как единственный runtime слой для локализации.
Intl как базового
механизма