A story about Moment.js performance

David Xu
WhereTo Engineering
4 min readSep 12, 2019

Moment.js is one of the most popular date parsing and formatting libraries for JavaScript. At WhereTo, we use Node.js so including Moment.js seemed like a natural fit. After all, we were using it on the front-end to render dates since the beginning, so what could be the problem?

Recently we’ve increased the number of flights our system returns about tenfold, and at the same time saw a dramatic drop in search performance. We were seeing the render loop go from taking less than 100ms to over 3 seconds to render ~5000 search results. Our team started investigating, and after profiling a few times, we noticed that over 99% of the time was spent in a single function, createInZone:

createInZone taking over 3.3 seconds of total time

Upon further investigation, we found out that this is called by Moment’s parseZone function. Why is it so slow? It seems that Moment.js was designed to account for the general use case, and as such it will attempt to parse the input string in a variety of ways. After reading the documentation a bit more, we found out that parseZone also accepts a date-format argument:

moment.parseZone(input, [format])

The first thing we did was we tried to use that variant of the parseZone function, but to no avail:

$ node bench.jsmoment#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)

While parseZone with the format parameter was slightly faster, it wasn’t fast enough for our needs.

Optimizing For Your Use Case

We were using Moment.js to parse dates coming back from our provider’s (Travelport) API, and realized that the format that they return dates in is always the same:

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

Knowing this, we set out to understand how Moment.js worked under the hood in order to (hopefully) write a much more efficient function to do the same.

Creating the fastest parseZone

The first thing we needed to understand was how Moment.js objects actually look under the hood. This was pretty simple to figure out:

> 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}

The next step is to create a function that can construct a Moment instance bypassing the constructor:

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

Now, it looks like there are multiple properties on the instance of Moment that we can just set (I won’t go into how we know this, but if you look at the source code of Moment.js you will understand)

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
}

The last step is to figure out how to parse the offset part of the timestamp. It turns out that this is always at the exact same position in the string, so we can optimize this part too!

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

Putting this all together, we end up with:

What About Benchmarks?

We’ve benchmarked this using the benchmark npm module. The code for the test suite is below:

After running this, we end up with:

$ node fastMoment.bench.jsmoment#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

We have successfully managed to parse timestamps into moment.js instances about 64 times faster! But what does this look like in practice? After profiling again, we get:

parseTravelportTimestamp taking a total of less than 40ms

The result is crazy: We went from over 3.3 seconds spent parsing dates to less than 40ms.

Lessons Learned

When we began working on our platform, the amount of tasks that we had to complete was staggering. Our daily mantra was, “Let’s first get this working, we can optimize it later.”

Over the past several years, the complexity of our product has grown tremendously. Fortunately we’re now at a point where we can move past the first phase of our mantra and into our second.

Utility libraries have served us well in getting to this point, but we’ve learned that by creating a tool for our specific problems, we can reduce the amount of overhead in our software and save users valuable time.

--

--