Tree shaking неиспользуемых локалей

Intl в JavaScript опирается на ICU (International Components for Unicode) — набор данных, включающий правила форматирования дат, чисел, валют, сортировки и языковые нормы для сотен локалей. Каждая локаль содержит значительный объём метаданных: названия месяцев, правила склонений, календарные особенности, символы валют, порядки сортировки и множество культурных нюансов.

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


ICU-данные и стоимость локалей

Реализация Intl в движках (V8, JavaScriptCore, SpiderMonkey) может использовать разные уровни ICU:

  • full-icu — полный набор локалей
  • small-icu — ограниченный набор (обычно английский + базовые)
  • system-icu — системные локали платформы

Node.js и браузеры отличаются по тому, какие локали включены и доступны. Например, Node.js может быть собран без полного ICU, и тогда:

new Intl.DateTimeFormat('ru-RU').format(new Date());
// может работать некорректно или fallback'иться на en-US

Для браузеров ситуация стабильнее, но при бандлинге возникает другая проблема: не сам Intl API увеличивает размер, а полифиллы и библиотеки поверх него.


Где возникает раздувание: не Intl, а полифиллы и i18n-библиотеки

Сам по себе Intl не попадает в бандл — он встроен в рантайм. Однако экосистема часто добавляет:

  • @formatjs/intl-*
  • intl-messageformat
  • react-intl
  • moment с локалями
  • date-fns/locale/*
  • luxon с набором локалей

Именно здесь появляются десятки и сотни килобайт локализационных данных.

Пример проблемы:

import { format } from 'date-fns';
import ru from 'date-fns/locale/ru';
import en from 'date-fns/locale/en';
import de from 'date-fns/locale/de';

Даже если используется только одна локаль, часто подключаются все.


Tree shaking и локали: базовая идея

Tree shaking — удаление неиспользуемого кода при сборке (ESM + статический анализ импортов).

Для локалей это означает:

  • если локаль импортируется явно → она попадает в бандл
  • если не используется → может быть удалена
  • если импорт “динамический или индексный” → tree shaking ломается

Проблема локалей в том, что они часто экспортируются как большие объектные карты:

export const locales = {
  en,
  ru,
  de,
  fr,
  ...
};

При таком подходе bundler видит использование locales целиком и не может выкинуть лишнее.


Почему Intl API сам по себе “tree-shakable”

В отличие от библиотек, Intl:

  • не импортируется как модуль
  • не содержит JS-локалей в бандле
  • использует данные рантайма движка

Поэтому:

new Intl.NumberFormat('ru-RU').format(12345.6);

не увеличивает размер бандла вообще.

Ключевая мысль: tree shaking применим не к Intl, а к обвязкам вокруг него.


Паттерны, которые ломают tree shaking локалей

1. Баррель-экспорты локалей

export * from './locales';

или

import * as locales from './locales';

Такой код превращает локали в неделимый объект.


2. Использование массива всех локалей

const allLocales = [en, ru, de, fr, es];

Даже если используется только ru, bundler обязан сохранить весь массив.


3. Динамический доступ по строке

const locale = locales[userLocale];

Если locales импортирован целиком, tree shaking невозможен.


Условия, при которых tree shaking локалей работает

1. Явные именованные импорты

import ru from 'date-fns/locale/ru';
import en from 'date-fns/locale/en';

Bundler может удалить всё остальное.


2. ESM-структура пакета

Tree shaking работает только при:

  • ES modules (import/export)
  • отсутствии side effects
  • корректном package.json:
{
  "sideEffects": false
}

3. Изоляция локалей в отдельных файлах

// locales/ru.js
export default { ... }
import ru from './locales/ru.js';

Intl + ручной список локалей: безопасная архитектура

Вместо импорта “всего мира локалей” используется стратегия whitelist:

const SUPPORTED_LOCALES = ['en-US', 'ru-RU'];

function formatDate(date, locale) {
  const safeLocale = SUPPORTED_LOCALES.includes(locale)
    ? locale
    : 'en-US';

  return new Intl.DateTimeFormat(safeLocale).format(date);
}

Здесь нет локалей в бандле — только строки.


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

Для библиотек, где локали всё же являются JS-модулями, используется ленивый импорт:

async function loadLocale(locale) {
  switch (locale) {
    case 'ru':
      return import('date-fns/locale/ru');
    case 'de':
      return import('date-fns/locale/de');
    default:
      return import('date-fns/locale/en-US');
  }
}

Такой подход переносит локали в code splitting, а не в основной бандл.


Webpack, Rollup и анализ локалей

Webpack

Webpack способен tree shake локали при:

  • production mode
  • ES modules
  • отсутствии CommonJS-обёрток

Однако часто локали приходят через CommonJS, что ломает анализ.


Rollup

Rollup точнее анализирует зависимости и лучше удаляет неиспользуемые локали при ESM.


Vite

Vite использует Rollup на этапе build, поэтому поведение аналогично, но dev-режим вообще не бандлит локали заранее.


ICU и форматирование без библиотек

Использование чистого Intl почти всегда предпочтительно:

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

nf.format(12345.67);

Здесь:

  • нет импортов локалей
  • нет зависимости от tree shaking
  • размер бандла не меняется

Частые ошибки при работе с локалями

1. Импорт всей локализационной библиотеки ради одной функции

import moment from 'moment';
import 'moment/locale/ru';
import 'moment/locale/de';
import 'moment/locale/fr';

Moment включает локали даже при частичном импорте, что делает tree shaking фактически неэффективным.


2. Использование “централизованного i18n объекта”

import locales from './locales';

Любая ссылка на такой объект блокирует удаление неиспользуемых частей.


3. Переоценка возможностей tree shaking

Tree shaking не работает при:

  • runtime-выборе через строки без статического анализа
  • CommonJS зависимостях
  • побочных эффектах модулей

Стратегии минимизации локалей в продакшене

  • использовать только Intl вместо полифиллов
  • ограничивать набор поддерживаемых локалей
  • импортировать локали по одному файлу
  • избегать barrel-экспортов для i18n
  • разделять локали по чанкам (code splitting)
  • проверять bundle analyzer (webpack-bundle-analyzer, rollup visualizer)

Поведение в крупных приложениях

В больших SPA и SSR-приложениях основная оптимизация локалей достигается не через сам Intl, а через:

  • архитектуру импорта i18n-слоя
  • контроль зависимостей библиотек форматирования
  • строгую модульную структуру локалей
  • разделение runtime и translation data

Intl остаётся базовым слоем, на который накладываются более тяжёлые i18n-системы, и именно они определяют, будет ли tree shaking эффективным или нет.