Рубрики
Разработка

Вам наверное не нужен Moment.js или другая библиотека для работы с датами

История одной маленькой оптимизации.

Сразу скажу, если ваш проект подразумевает много работы с датами, поддержу различных локалей и прочее, то вам нужна библиотека, но этот пост о другом.

Работая над очередным проектом я пытался найти возможности оптимизировать размер загружаемых JavaScript файлов. Я заметил, что некоторые бандлы довольно небольших Vue.js приложений даже в оптимизированном под продакшн виде имеют несопоставимый своим задачам размер. Я не эксперт во Vue.js, но знаю, что одним из аргументов в его пользу есть то, что размер самого фреймворка довольно компактный, поэтому я решил посмотреть на код самих приложений.

Вывод даты с кастомным форматированием для одной локали

Одно приложение содержало список новостей с датами и для того, чтобы форматировать дату была использована (к счастью) не вся библиотека Moment.js, а довольно популярное расширение с набором фильтров даты для Vue.js – vue-moment (основано на Moment.js). По сравнению со всей библиотекой это расширение имеет меньший размер, порядка 55,9 Kb в минимизированном виде, но и это дороговато за один фильтр формата даты.

Вот такой фильтр будет стоить вам полсотни килобайт:

moment("MMMM D, YYYY, h:mm a")

На самом же деле, если вам не нужно поддерживать действительно древние браузеры, то все можно решить в пару десятков строк кода. С использованием ECMAScript Internationalization API, а именно конструктора Intl.DateTimeFormat(), все становиться куда экономичнее.

Vue.filter('formatArticleDate', function(value) {
  if (value) {
    const dateObj = new Date(value);
    // Указываем желаемую временную зону для которой делать поправку
    const timeZone = 'America/New_York';
    // Определяем локаль для форматирования
    const locale = 'en-US';
    // Формат отображения для года, месяца, дня, часов, минут 
    const dateFormatOptions = {
      month: 'short',
      day: 'numeric',
      year: 'numeric',
      hour: 'numeric',
      minute: 'numeric'
    }
    try {
      // Если браузер поддерживает Intl API
      return `${new Intl.DateTimeFormat(locale, { ...dateFormatOptions, timeZone }).format(dateObj)}`;
    } catch (e) {
      try {
        // Если не поддерживается timeZone (IE 11)
        return `${new Intl.DateTimeFormat(locale, { dateFormatOptions }).format(dateObj)}, ${dateObj.toLocaleTimeString(locale, {hour: '2-digit', minute: '2-digit'})}`;
      } catch (error) {
        // Если все совсем плохо вернем дату в виде UTC строки 
        return dateObj.toUTCString();
      }
    }
  }
});

В итоге 25 строк кода сэкономят вам 55,9 Kb размера бандла.

В данном примере мы жестко указываем локаль для форматра, но мы можем поступить куда более продуктивно, если нам не нужен IE 11. Мы можем передавать правильную локаль для пользователя просто передавая в качестве значение локали содержимое свойства navigator.languages, как правило это массив строк со значениями локалей в зависимости от настроек вашего браузера. Конструктор сам позаботится о том, чтобы распарсить массив и взять нужное значение. Кстати, можно также использовать navigator.language, который вернет строку с локалью по умолчанию для браузера.

Работа с датами используя нативный API вместо Moment.js

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

...
let startTime = moment.unix(latest_news_timestamp);	                                let startTimeHours = moment(startTime, "HH");	                                let hours = moment().diff(startTimeHours, 'hours');	                                                
// Определяем, что новость была добавлена 24, 96 или более часов назад.
// Значение 1 для менее чем 24, 2 для менее чем 96, 0 для более чем 96 часов назад.
if (hours <= 96) {
  if (hours <= 24) {
...

Как видим, для довольно тривиальных задач разработчик использовал методы Moment.js вместо возможностей которые нам дает математика и конструктор Date(). Все это можно было переписать приблизительно так:

...
// Текущий момент в миллисекундах.
const now  = Date.now();
// Переводим значение временной метки публикации в Unix формате в миллисекунды.
const newsStartTime = latest_news_timestamp*1000;
// Определяем разницу значений и переводим в часы (3600000 = миллисекунд в часе).
const diffH = Math.round((now - newsStartTime) / 3600000);
// Определяем, что новость была добавлена 24, 96 или более часов назад.
// Значение 1 для менее чем 24, 2 для менее чем 96, 0 для более чем 96 часов назад.
if (diffH <= 96) {
 if (diffH <= 24) {
...

Здесь даже значение количества фактических строк кода не поменялось, если не брать во внимание комментарии. Но по сути мы получили то же поведение, однако без использования Moment.js, что в итоге помогло снизить размер минимизированного бандла на 496 Kb!

Вот еще один пример:

// Получаем дату в формате 'YYYY-MM-DD', за 20 дней до сегодня.
let date = moment().subtract(20, 'days').format('YYYY-MM-DD');

Это же можно сделать нативно так:

// Сейчас в миллисекундах.
const now = Date.now();
// 20 дней назад в миллисекундах.
const daysAgo = now - (3600000*24*20);
// Формируем дату в формате ISOString (2021-02-17T13:32:59.669Z) и просто берем 'YYYY-MM-DD' из строки. 
const date = new Date(daysAgo).toISOString().replace(/T.+/gi, '');

В случае если бы нам понадобились более изощренные форматы даты мо могли бы их получить используя тот же Intl.DateTimeFormat или Date.prototype.toLocaleDateString().

Date.toLocaleTimeString() и Date.toLocaleDateString() как альтернатива Internationalization API?

В качестве альтернативы Intl.DateTimeFormat вы можете использовать методы Date.prototype.toLocaleTimeString() и Date.prototype.toLocaleDateString(). Однако никакой реальной выгоды вы не получаете. Не смотря на то, что базовая реализация обоих методов имеет более широкую поддержку браузеров, современные фичи, как опция timeZone, не работают в IE 11 и фактически мы получаем те же ограничения совместимости, что для Internationalization API. Но стоит отметить, что использование метода format() объекта Intl.DateTimeFormat является более производительным решением для обработки большого количества дат.

Итоги

Подводя итоги всего вышеизложенного можно сказать следующее:

  • знание основ JavaScript и современных API должно идти впереди знания фреймворков и библиотек;
  • плохим наш код делает не фреймворк или библиотека, а мы;
  • если ваш проект не требует большого количества фич для работы с датами и вам не нужна 100% поддержка огромного количества локалей, то вам определенно стоит посмотреть в сторону с Intl.DateTimeFormat;
  • в случае, если же вам не хватает того, что может предложить современное API для работы с датами на вашем проекте, то вам определенно не нужен Moment.js хотя бы потому, что сами разработчики переводят проект в режим поддержки с последующим закрытием.

В качестве альтернатив Moment.js вы можете рассмотреть, как вариант, такие библиотеки:

Удачного кодинга!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *