Замена moment.js и date-fns

Библиотеки работы с датами исторически решали проблему отсутствия удобного API в JavaScript. Однако архитектура современного ECMAScript и развитие стандартного Intl API сделали значительную часть их функциональности избыточной.

Основные проблемы, которые приводят к отказу от moment.js и частично от date-fns:

  • избыточный размер бандла (особенно moment.js)
  • необходимость ручной локализации
  • дублирование возможностей браузера и runtime
  • устаревшая модель мутабельных объектов (moment.js)
  • отсутствие нативной поддержки ICU-данных в пользовательском коде
  • сложность tree-shaking в старых архитектурах библиотек

Современный подход заключается в использовании встроенных международных API, предоставляемых движком Jav * aScript: Intl.DateTimeFormat, Intl.RelativeTimeFormat, Intl.NumberFormat, Intl.Collator, Intl.PluralRules.


Intl.DateTimeFormat как основа форматирования дат

Ключевой компонент замены moment.js — Intl.DateTimeFormat. Он позволяет форматировать даты в зависимости от локали без внешних зависимостей.

Базовое форматирование

const formatter = new Intl.DateTimeFormat('ru-RU', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

formatter.format(new Date());

Вместо:

moment().format('LL');

Разница принципиальная: формат определяется не строковым шаблоном, а набором опций, интерпретируемых ICU.


Форматирование времени

const timeFormatter = new Intl.DateTimeFormat('ru-RU', {
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit'
});

timeFormatter.format(new Date());

Эквивалент moment().format('HH:mm:ss') без необходимости подключения библиотеки.


Локализация без ручных словарей

moment.js требует подключения локалей:

import 'moment/locale/ru';
moment.locale('ru');

Intl API использует данные окружения:

new Intl.DateTimeFormat('ru-RU', { dateStyle: 'full' }).format(new Date());

Intl.RelativeTimeFormat вместо moment.fromNow()

Одно из наиболее частых применений moment.js — относительное время.

Реализация через Intl

const rtf = new Intl.RelativeTimeFormat('ru', { numeric: 'auto' });

rtf.format(-1, 'day'); // "вчера"
rtf.format(3, 'day');  // "через 3 дня"

Замена moment.fromNow()

// moment
moment().subtract(5, 'minutes').fromNow();

// Intl
const rtf = new Intl.RelativeTimeFormat('ru', { numeric: 'auto' });

rtf.format(-5, 'minute');

Построение утилиты уровня moment

function formatRelative(date) {
  const diff = (date - new Date()) / 1000;

  const units = [
    ['year', 31536000],
    ['month', 2592000],
    ['day', 86400],
    ['hour', 3600],
    ['minute', 60],
    ['second', 1]
  ];

  const rtf = new Intl.RelativeTimeFormat('ru', { numeric: 'auto' });

  for (const [unit, seconds] of units) {
    const delta = diff / seconds;
    if (Math.abs(delta) >= 1) {
      return rtf.format(Math.round(delta), unit);
    }
  }

  return rtf.format(0, 'second');
}

Intl.NumberFormat вместо форматирования чисел и валют

date-fns не покрывает валюты, moment вообще не предназначен для этого, но в реальных приложениях это часто соседняя задача.

Валюты

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

currency.format(1500);

Проценты и единицы измерения

new Intl.NumberFormat('ru-RU', {
  style: 'percent'
}).format(0.25);
new Intl.NumberFormat('ru-RU', {
  style: 'unit',
  unit: 'kilometer'
}).format(10);

Intl.Collator для сортировки без lodash и custom compare

Сортировка строк с учётом языка — одна из причин появления вспомогательных функций в date-fns-подобных экосистемах.

const collator = new Intl.Collator('ru', { sensitivity: 'base' });

['яблоко', 'Ёж', 'арбуз'].sort(collator.compare);

Intl.PluralRules вместо ручных условий множественного числа

moment.js и date-fns не решают задачу склонений.

const pr = new Intl.PluralRules('ru');

pr.select(1); // one
pr.select(2); // few
pr.select(5); // many

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

const forms = {
  one: 'день',
  few: 'дня',
  many: 'дней',
  other: 'дня'
};

function formatDays(n) {
  const rule = new Intl.PluralRules('ru').select(n);
  return `${n} ${forms[rule]}`;
}

Композиция Intl API как замена date-fns

date-fns строится как набор функций:

format()
addDays()
subMonths()
differenceInDays()

Intl API не работает как библиотека манипуляции датами, но закрывает слой представления.

Архитектурное разделение

  • манипуляция датами → нативный Date или Temporal (в будущем)
  • форматирование → Intl.DateTimeFormat
  • относительность → Intl.RelativeTimeFormat
  • локализация чисел → Intl.NumberFormat
  • сортировка → Intl.Collator

Пример замены типичного moment.js сценария

moment.js подход

moment(date)
  .add(3, 'days')
  .locale('ru')
  .format('D MMMM YYYY');

современный подход

const date = new Date();
date.setDate(date.getDate() + 3);

const formatter = new Intl.DateTimeFormat('ru-RU', {
  day: 'numeric',
  month: 'long',
  year: 'numeric'
});

formatter.format(date);

Кэширование форматтеров как обязательная оптимизация

Intl-объекты дорогие в создании, поэтому их переиспользование критично.

const formatters = {
  ruLongDate: new Intl.DateTimeFormat('ru-RU', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
};

formatters.ruLongDate.format(new Date());

В высоконагруженных системах создание форматтера внутри функции становится узким местом.


Особенности локалей и ICU

Intl API напрямую зависит от ICU-данных, встроенных в runtime:

  • Node.js может поставляться с полной или slim ICU
  • браузеры имеют собственные наборы локалей
  • поведение форматирования может отличаться между окружениями

Это делает Intl более системным API, чем библиотечным.


Сравнение подходов moment.js, date-fns и Intl

moment.js

  • единый объект состояния
  • цепочки методов
  • мутабельность
  • тяжёлый бандл

date-fns

  • функциональный стиль
  • tree-shaking
  • хорош для манипуляций
  • ограничен в i18n

Intl API

  • встроенный runtime API
  • ICU-локализация
  • минимальный бандл
  • отсутствие манипуляции датами

Типовые миграционные соответствия

moment.js / date-fns Intl API
format() Intl.DateTimeFormat
fromNow() Intl.RelativeTimeFormat
formatCurrency() Intl.NumberFormat (currency)
sort strings Intl.Collator
plural rules Intl.PluralRules

Практическая модель современной архитектуры дат

Современный стек работы с датами в JavaScript фактически разделяется на три уровня:

  1. Дата как значение

    • Date
    • Temporal (экспериментально)
  2. Логика

    • чистые функции
    • арифметика времени
  3. Представление

    • Intl.DateTimeFormat
    • Intl.RelativeTimeFormat
    • Intl.NumberFormat

Разделение этих слоёв устраняет необходимость в универсальной библиотеке уровня moment.js и снижает связность кода с внешними зависимостями.