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

Проверка поддержки возможностей Intl

Интерфейс Intl в современных JavaScript-окружениях реализован неравномерно: базовые классы могут присутствовать, но отдельные части API (например, Intl.PluralRules, Intl.RelativeTimeFormat, Intl.Segmenter) часто отсутствуют или имеют ограниченную реализацию.

Типичная стратегия начинается с определения доступности конкретной функции:

const hasDateTimeFormat = typeof Intl !== "undefined" &&
  typeof Intl.DateTimeFormat === "function";

const hasPluralRules = typeof Intl !== "undefined" &&
  typeof Intl.PluralRules === "function";

const hasRelativeTimeFormat = typeof Intl !== "undefined" &&
  typeof Intl.RelativeTimeFormat === "function";

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


Стратегия динамической подгрузки

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

Базовая схема:

async function ensurePluralRules() {
  if (typeof Intl.PluralRules !== "function") {
    await import("@formatjs/intl-pluralrules/polyfill");
    await import("@formatjs/intl-pluralrules/locale-data/ru");
  }
}

Ключевая идея заключается в ленивой инициализации: полифил подключается только при первом обращении к соответствующему API.


Разделение полифилов по функциональности

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

  • Intl.DateTimeFormat — форматирование дат и времени
  • Intl.NumberFormat — числовые форматы
  • Intl.PluralRules — правила множественных форм
  • Intl.RelativeTimeFormat — относительное время
  • Intl.Segmenter — сегментация текста

Каждый модуль может иметь собственный полифил, что делает возможной точечную загрузку:

async function ensureRelativeTimeFormat() {
  if (typeof Intl.RelativeTimeFormat !== "function") {
    await import("@formatjs/intl-relativetimeformat/polyfill");
    await import("@formatjs/intl-relativetimeformat/locale-data/ru");
  }
}

Инициализация локалей как отдельный слой

Большинство полифилов Intl требуют загрузки языковых данных отдельно. Это важный фактор оптимизации: сами полифилы часто малы, но локали могут занимать значительный объём.

Структура загрузки:

async function loadIntlLocale(locale) {
  switch (locale) {
    case "ru":
      await import("@formatjs/intl-pluralrules/locale-data/ru");
      await import("@formatjs/intl-relativetimeformat/locale-data/ru");
      break;
    case "en":
      await import("@formatjs/intl-pluralrules/locale-data/en");
      await import("@formatjs/intl-relativetimeformat/locale-data/en");
      break;
  }
}

Разделение логики позволяет избежать загрузки ненужных языков.


Ленивое подключение через обёртки

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

let pluralRulesPromise;

function getPluralRules(locale) {
  if (!pluralRulesPromise) {
    pluralRulesPromise = (async () => {
      if (typeof Intl.PluralRules !== "function") {
        await import("@formatjs/intl-pluralrules/polyfill");
      }
      return new Intl.PluralRules(locale);
    })();
  }

  return pluralRulesPromise;
}

Подобная схема полезна для предотвращения повторных загрузок и гонок инициализации.


Условная загрузка на уровне функций форматирования

Часто Intl используется точечно: форматирование дат, валют, сообщений. Это позволяет строить ленивые функции-обёртки:

export async function formatDate(date, locale) {
  if (typeof Intl.DateTimeFormat !== "function") {
    await import("@formatjs/intl-datetimeformat/polyfill");
    await import("@formatjs/intl-datetimeformat/locale-data/ru");
  }

  const formatter = new Intl.DateTimeFormat(locale, {
    year: "numeric",
    month: "long",
    day: "numeric"
  });

  return formatter.format(date);
}

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


Использование современных bundler-стратегий

Инструменты сборки влияют на эффективность динамической загрузки.

Webpack

Webpack поддерживает код-сплиттинг через import():

const loadIntl = () => import("./intl-polyfills");

Дальнейшая сегментация позволяет разделить:

  • базовые полифилы
  • локальные данные
  • редко используемые функции

Vite

Vite использует ES-модули и позволяет автоматически разделять чанки:

if (!Intl.RelativeTimeFormat) {
  await import("intl-relativetimeformat");
}

Изоляция окружений: клиент и сервер

В серверном окружении (Node.js) поведение Intl может отличаться от браузерного. В некоторых версиях Node поддержка расширяется, но не всегда совпадает с клиентской.

Условная логика:

const isBrowser = typeof window !== "undefined";

async function ensureIntl() {
  if (!isBrowser) return;

  if (typeof Intl.RelativeTimeFormat !== "function") {
    await import("@formatjs/intl-relativetimeformat/polyfill");
  }
}

Такой подход предотвращает избыточную загрузку в SSR.


Кэширование загруженных полифилов

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

const cache = new Map();

async function loadPolyfill(key, importer) {
  if (!cache.has(key)) {
    cache.set(key, importer());
  }
  return cache.get(key);
}

Использование:

await loadPolyfill("pluralRules", async () => {
  if (typeof Intl.PluralRules !== "function") {
    await import("@formatjs/intl-pluralrules/polyfill");
  }
});

Гранулярная загрузка по возможностям браузера

Фрагментация API позволяет минимизировать загрузку:

  • проверка конкретной функции
  • загрузка только нужного полифила
  • добавление только нужной локали

Пример комбинированной стратегии:

async function ensureIntlCapabilities(locale) {
  if (typeof Intl.DateTimeFormat !== "function") {
    await import("@formatjs/intl-datetimeformat/polyfill");
  }

  if (typeof Intl.NumberFormat !== "function") {
    await import("@formatjs/intl-numberformat/polyfill");
  }

  if (typeof Intl.RelativeTimeFormat !== "function") {
    await import("@formatjs/intl-relativetimeformat/polyfill");
  }

  await import(`@formatjs/intl-pluralrules/locale-data/${locale}`);
}

Слои деградации функциональности

При отсутствии полного Intl API возможна каскадная деградация:

  1. Нативная реализация браузера
  2. Частичная реализация Intl
  3. Полифил конкретного модуля
  4. Упрощённая JS-реализация

Пример fallback-цепочки:

function formatNumber(value, locale) {
  if (typeof Intl.NumberFormat === "function") {
    return new Intl.NumberFormat(locale).format(value);
  }

  return String(value);
}

Оптимизация загрузки в многолокальных приложениях

При поддержке нескольких языков загрузка локалей становится критической точкой. Стратегия:

  • базовая локаль загружается при старте
  • дополнительные локали загружаются по требованию
  • удаление неиспользуемых локалей на этапе сборки
async function switchLocale(locale) {
  await import(`@formatjs/intl-numberformat/locale-data/${locale}`);
  await import(`@formatjs/intl-datetimeformat/locale-data/${locale}`);
}

Изоляция полифилов по feature flags

В крупных приложениях Intl-функции часто управляются через feature flags:

const features = {
  relativeTime: true,
  segmenter: false
};

async function initIntl() {
  if (features.relativeTime && typeof Intl.RelativeTimeFormat !== "function") {
    await import("@formatjs/intl-relativetimeformat/polyfill");
  }

  if (features.segmenter && typeof Intl.Segmenter !== "function") {
    await import("@formatjs/intl-segmenter/polyfill");
  }
}

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


Работа с Intl Segmenter как пример тяжёлого полифила

Intl.Segmenter относится к наиболее современным частям API и часто отсутствует в браузерах.

async function ensureSegmenter() {
  if (typeof Intl.Segmenter !== "function") {
    await import("@formatjs/intl-segmenter/polyfill");
  }

  return new Intl.Segmenter("ru", { granularity: "word" });
}

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


Согласование с архитектурой модульного приложения

Динамические полифилы хорошо вписываются в модульную архитектуру:

  • UI-модули инициируют форматирование
  • сервисный слой управляет загрузкой Intl
  • инфраструктурный слой содержит кэш и feature detection

Разделение ответственности позволяет избегать дублирования загрузок и упрощает контроль над зависимостями Intl API.