8 800 201 6-48-61Для бесплатных звонков на территории РФ
Наше портфолио
на основных мобильных площадках

Производительность библиотеки Moment.js

Moment.js — это одна из самых популярных JavaScript-библиотек для разбора и форматирования дат. Для многих применение этой библиотеки было совершенно естественным ходом. Проблем с серверным использованием Moment.js не ожидалось. В конце концов, с самого начала использовали эту библиотеку во фронтенде для вывода дат и были довольны её работой. Однако то, что библиотека хорошо показала себя на клиенте, ещё не означало, что и на сервере с ней проблем не будет.


Рост проекта и падение производительности

Недавно число записей о самолётных рейсах, которые возвращает система WhereTo, выросло примерно в десять раз. Тогда мы столкнулись с очень сильным падением производительности. Оказалось, что цикл рендеринга, который занимал менее 100 миллисекунд, теперь, для вывода около 5000 результатов поиска, выполняется более 3 секунд. Наша команда занялась исследованиями. После нескольких сеансов профилирования мы заметили, что более 99% этого времени тратится в единственной функции, которая называется createInZone.


На выполнение функции createInZone приходится около 3.3 секунд

Продолжив исследование ситуации, мы обнаружили, что эта функция вызывается функцией Moment.js parseZone. Почему она такая медленная? У нас было такое ощущение, что библиотека Moment.js была создана в расчёте на распространённые сценарии её использования, и в результате она будет пытаться обработать входную строку различными способами. Может быть стоит ограничить её? После того, как мы вчитались в документацию, мы выяснили, что функция parseZone принимает необязательный аргумент, задающий формат даты:

moment.parseZone(input, [format])

Первым, что мы сделали, была попытка использовать функцию parseZone с передачей ей информации о формате даты, но это, как показали тесты производительности, ни к чему не привело:

$ node bench.js
moment#parseZone x 22,999 ops/sec ±7.57% (68 runs sampled)
moment#parseZone (with format) x 30,010 ops/sec ±8.09% (77 runs sampled)

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


Оптимизация с учётом особенностей проекта

Мы использовали Moment.js для парсинга дат, получаемых из API нашего провайдера (Travelport). Мы поняли, что он всегда возвращает данные в одном и том же формате:

"2019-12-03T14:05:00.000-07:00"

Зная это, мы начали разбираться во внутреннем устройстве Moment.js для того, чтобы (как мы надеялись) написать гораздо более эффективную функцию, выдающую те же результаты.


Создание более быстрой альтернативы parseZone

Для начала нам нужно было разобраться в том, как выглядят объекты Moment.js. Понять это было довольно просто:

> const m = moment()
> console.log(m)
Moment { _isAMomentObject: true, _i: '2019-12-03T14:05:00.000-07:00', _f: 'YYYY-MM-DDTHH:mm:ss.SSSSZ', _tzm: -420, _isUTC: true, _pf: { ...snip }, _locale: [object Locale], _d: 2019-12-03T14:05:00.000Z, _isValid: true, _offset: -420}

Следующим шагом было создание экземпляра Moment без использования конструктора:

export function parseTravelportTimestamp(input: string) { const m = {} // $FlowIgnore m.__proto__ = moment.prototype return m
}

Теперь возникало такое ощущение, что у нас имеется множество свойств экземпляра Moment, которые мы можем просто установить (я не вдаюсь в детали того, как мы об этом узнали, но если вы посмотрите исходный код Moment.js — вы это поймёте):

const FAKE = moment()const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ'export function parseTravelportTimestamp(input: string) { const m = {} // $FlowIgnore m.__proto__ = moment.prototype const offset = 0 // TODO const date = new Date(input.slice(0, 23)) m._isAMomentObject = true m._i = input m._f = TRAVELPORT_FORMAT m._tzm = offset m._isUTC = true m._locale = FAKE._locale m._d = date m._isValid = true m._offset = offset return m
}

Последний шаг нашей работы заключался в том, чтобы выяснить то, как надо разбирать значение offset отметки времени. Оказалось, что это — всегда одна и та же позиция в строке. В результате мы смогли оптимизировать и это:

function parseTravelportDateOffset(input: string) { const hrs = +input.slice(23, 26) const mins = +input.slice(27, 29) return hrs * 60 + (hrs < 0 ? -mins : mins)
}

Вот что получилось после того, как мы всё это собрали:

const FAKE = moment()const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ'function parseTravelportDateOffset(input: string) { const hrs = +input.slice(23, 26) const mins = +input.slice(27, 29) return hrs * 60 + (hrs < 0 ? -mins : mins)
}/** * Обрабатываются только даты формата ISO-8601, представленные в следующем виде: * - "2019-12-03T12:30:00.000-07:00" */export function parseTravelportTimestamp(input: string): moment { const m = {} // $FlowIgnore m.__proto__ = moment.prototype const offset = parseTravelportDateOffset(input) const date = new Date(input.slice(0, 23)) m._isAMomentObject = true m._i = input m._f = TRAVELPORT_FORMAT m._tzm = offset m._isUTC = true m._locale = FAKE._locale m._d = date m._isValid = true m._offset = offset return m
}



Испытания производительности

Мы провели испытания производительности получившегося решения с использованием npm-модуля benchmark. Вот код бенчмарка:

const FAKE = moment()const TRAVELPORT_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSSZ'function parseTravelportDateOffset(input: string) { const hrs = +input.slice(23, 26) const mins = +input.slice(27, 29) return hrs * 60 + (hrs < 0 ? -mins : mins)
}/** * Обрабатываются только даты формата ISO-8601, представленные в следующем виде: * - "2019-12-03T12:30:00.000-07:00" */export function parseTravelportTimestamp(input: string): moment { const m = {} // $FlowIgnore m.__proto__ = moment.prototype const offset = parseTravelportDateOffset(input) const date = new Date(input.slice(0, 23)) m._isAMomentObject = true m._i = input m._f = TRAVELPORT_FORMAT m._tzm = offset m._isUTC = true m._locale = FAKE._locale m._d = date m._isValid = true m._offset = offset return m
}

Вот какие результаты исследования производительности у нас получились:

$ node fastMoment.bench.js
moment#parseZone x 21,063 ops/sec ±7.62% (73 runs sampled)
moment#parseZone (with format) x 24,620 ops/sec ±6.11% (71 runs sampled)
fast#parseTravelportTimestamp x 1,357,870 ops/sec ±5.24% (79 runs sampled)
Fastest is fast#parseTravelportTimestamp

Как оказалось, нам удалось ускорить разбор отметок времени примерно в 64 раза. Но как это повлияло на реальную работу системы? Вот что получилось в результате профилирования.


Общее время выполнения parseTravelportTimestamp составляет менее 40 мс.

Результаты оказались просто потрясающими: мы начинали с 3.3 секунд, уходящих на разбор дат, а пришли к менее чем 40 миллисекундам.


Итоги

Когда мы начинали работать над нашей платформой, нам предстояло решить просто страшное количество задач. Мы только и знали, что повторяли себе: «Пусть сначала заработает, а заняться оптимизацией можно и позже».

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

Библиотечные решения помогли проекту добраться до того места, где он находится в наши дни. Но мы столкнулись с «библиотечной» проблемой. Решая её, мы узнали о том, что, создав собственный механизм, ориентированный на наши нужды, мы можем сделать код «легче» в плане потребления системных ресурсов и сэкономить ценное время наших пользователей.

Список статей
Реализованные проекты
Больше проектов
Среди наших клиентов
Полезная информация
Виды спорта для программистов
В новом году, команда Софт Медиа Систем решила более пристально следить за своим здоровьем. В этой статье, мы расскажем о видах физических нагрузок, которые практикует команда разработчиков.Тренажерный залДля тех, кто не знает с чего начать и как правильно заниматься, самым простым решением будет записаться в тренажерный зал и выполнять упражнения под присмотром тренера. Любой хороший фитнес-центр имеет команду тренеров, которые… Читать больше
Пять идей для приложений с геолокацией
В мире существует не так много приложений, которые были использовали геолокацию как основную фишку. Например, Foursquare – сервис по поиску интересных мест на карте. В 2016 появилась игра Pokemon Go и пользователям очень понравилась игра, которая использует данные о местонахождении пользователя. В нашей новой статье специалисты компании «Софт Медиа Систем» придумывают идеи для приложений в основе которых имеется идея использовать… Читать больше
Пять браузерных расширений для веб-разработчиков и дизайнеров
В нашей новой статье, команда Софт Медиа Систем рассказывает какие инструменты могут быть полезны дизайнерам и веб-разработчикам.ColorZilla Набор различных инструментов для работы с цветом. Для разных инструментов можно назначить горячие клавиши. Есть генератор градиентов, цветовой миксер и незаменимая в работе пипетка.Font swapДанное расширение покажет как будет выглядеть тот или иной шрифт на сайте. Шрифты берутся из библиотеки Google.… Читать больше
Больше статей
СВЯЖИТЕСЬ С НАМИ
Мы верим, что мобильные решения помогают бизнесу работать эффективнее.
Наша компания делает мобильную разработку доступной для бизнеса.
Сделать шаг к мобильности бизнеса еще никогда не было так просто!
Бизнес Парк «Румянцево»
108811, Москва, Киевское шоссе, 22-й км, вл. 4 блок Е.
Телефон: 8 (800) 201 6-48-6
E-mail: support@s-m-system.ru
Наше портфолио
ВСЁ ПРОСТО