Оптимизация bundle size

Использование встроенного Intl в JavaScript часто воспринимается как бесплатное с точки зрения размера бандла, однако в реальных приложениях влияние на итоговый размер сборки зависит от архитектуры проекта, используемых полифиллов и стратегии поддержки браузеров.

Основной источник роста bundle size связан не с самим Intl, а с дополнительными слоями, которые подключаются поверх него: полифиллы, библиотеки форматирования и локализационные данные (CLDR/ICU). При неправильной организации импорта эти зависимости способны увеличить итоговый бандл на сотни килобайт и более.


Встроенный Intl и отсутствие необходимости в сторонних библиотеках

Современные движки JavaScript предоставляют нативную реализацию Intl, включающую:

  • Intl.DateTimeFormat
  • Intl.NumberFormat
  • Intl.RelativeTimeFormat
  • Intl.PluralRules
  • Intl.Collator
  • Intl.ListFormat
  • Intl.Segmenter

Использование нативных API вместо библиотек вроде moment.js или date-fns/format позволяет устранить необходимость в тяжелых runtime-зависимостях.

Особенность заключается в том, что нативный Intl не попадает в JavaScript bundle вообще. Однако косвенные зависимости могут нивелировать это преимущество.


ICU и скрытая стоимость локализации

Главный фактор, влияющий на размер и поведение Intl, — ICU (International Components for Unicode).

В Node.js существуют разные режимы сборки:

  • minimal ICU (малый набор локалей)
  • full ICU (полный набор локалей)
  • system ICU (зависимость от системных библиотек)

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

  • full-icu
  • icu-data-regex
  • кастомные сборки Node.js

Полный ICU увеличивает размер бинарника Node.js, но не увеличивает client-side bundle. Однако в server-side rendering сценариях это напрямую влияет на размер деплоя.


Polyfill-стратегия и влияние на bundle size

На клиентской стороне основной источник увеличения bundle size — полифиллы Intl. Особенно это касается старых браузеров, где отсутствуют:

  • Intl.RelativeTimeFormat
  • Intl.ListFormat
  • Intl.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';

Такой подход позволяет:

  • исключить неиспользуемые локали
  • сократить размер initial bundle
  • перенести локализационные данные в отдельные чанки

Критический эффект достигается при большом количестве поддерживаемых языков, где full-таблицы CLDR могут превышать 1–2 MB.


Tree-shaking и проблема side effects

Современные бандлеры (Webpack, Vite, Rollup) способны удалять неиспользуемый код при наличии корректных ESM-экспортов.

Однако Intl-полифиллы часто содержат:

  • глобальные сайд-эффекты
  • регистрацию в globalThis.Intl
  • импорт побочных данных

Это делает tree-shaking частично неэффективным.

Пример проблемного импорта:

import '@formatjs/intl-numberformat/polyfill';

Даже если используется только NumberFormat для одной локали, весь пакет может быть включён в initial bundle из-за side effects.

Для снижения влияния применяется стратегия:

  • изоляция polyfill в отдельный entry point
  • динамический импорт

Динамическая загрузка локалей

Наиболее эффективный подход к оптимизации 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;
  }
}

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

  • исключить локали из initial bundle
  • загружать только используемые языки
  • использовать caching механизм браузера

В SPA архитектурах это особенно эффективно при смене языка в runtime.


Использование нативного Intl как стратегия минимизации зависимостей

Основной принцип оптимизации заключается в максимальном использовании встроенного 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 движка.


Разделение runtime и polyfill слоя

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

  • runtime слой (логика приложения)
  • intl слой (полифиллы и локали)

Пример структуры:

/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');
}

Влияние bundler-стратегий (Webpack, Vite, Rollup)

Webpack

Webpack требует явной настройки sideEffects:

{
  "sideEffects": false
}

Если библиотека Intl-полифиллов помечена как side-effect-free, tree-shaking становится эффективнее.

Также используется code splitting:

optimization: {
  splitChunks: {
    chunks: 'all'
  }
}

Vite

Vite по умолчанию использует ES modules и более агрессивный tree-shaking. Однако Intl-полифиллы всё равно могут попадать в initial chunk при static import.

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

  • dynamic import
  • pre-bundling control (optimizeDeps)

Замена тяжёлых библиотек форматирования на Intl

Многие проекты сохраняют legacy зависимости:

  • moment.js
  • luxon
  • dayjs (частично)

При переходе на Intl устраняется необходимость в:

  • timezone database внутри JS
  • парсерах строк дат
  • локализационных таблицах

Особенно значительное уменьшение bundle size наблюдается при отказе от moment-timezone, где размер может превышать 200–400 KB gzipped.


Lazy formatting как метод снижения initial bundle

Форматирование дат, чисел и списков часто не требуется при первом рендере приложения. Это позволяет отложить загрузку 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);
};

Оптимизация через ограничение локалей

Частая ошибка — включение всех локалей “на будущее”.

Правильная стратегия:

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

Пример анти-паттерна:

import * as locales from '@formatjs/intl-datetimeformat/locale-data';

Такой импорт гарантированно увеличивает bundle size до максимального объема доступных данных.


Сравнение влияния Intl-стратегий на bundle size

Стратегия Initial bundle Lazy loading Поддержка локалей
full polyfill высокий нет максимальная
partial polyfill средний частично ограниченная
native Intl минимальный не требуется зависит от окружения
hybrid (native + polyfill fallback) низкий да контролируемая

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

Наиболее устойчивый подход сочетает:

  • проверку наличия Intl API
  • подключение polyfill только при необходимости
  • загрузку локалей по требованию
if (!('RelativeTimeFormat' in Intl)) {
  await import('@formatjs/intl-relativetimeformat/polyfill');
  await import('@formatjs/intl-relativetimeformat/locale-data/ru');
}

Такая модель минимизирует initial bundle и сохраняет совместимость со старыми окружениями.


Косвенные зависимости и скрытый рост bundle

Оптимизация Intl часто блокируется не самим API, а сторонними библиотеками:

  • UI-фреймворки с встроенной локализацией
  • design systems с форматтерами дат
  • date utilities с дублирующим функционалом

Решение заключается в унификации источника форматирования: только Intl как единственный runtime слой для локализации.


Итоговые архитектурные принципы оптимизации

  • использование нативного Intl как базового механизма
  • исключение глобальных polyfill imports
  • разделение локалей на чанки
  • динамическая загрузка языков
  • минимизация ICU-данных в клиентском бандле
  • контроль side effects в зависимостях
  • отказ от тяжёлых date/time библиотек в пользу встроенных API