Why you shouldn't use Moment.js...

Why you shouldn't use Moment.js...

...or, at least, what you should remember while using it.

JavaScript's built-in Date object is far from ideal, let's admit it. We could spend a lot of time talking about Date API methods like getYear, however we believe everything has been told in that matter. Everyone of us, at some point realized that or has been warned by older friends, if lucky enough. Whatever the reason was, at the end we all reached for external library and the choice was obvious - Moment.js. The most popular JS DateTime library that gave us everything we wanted so much from native Date API.

If it is much better than native API, why do we advise you not to use it?

1. It's slow

Some time ago, we've been working on optimizing our client's project. Without delving into details - we had a function that had to be able to process more than 10 000 short timeframes. We found quickly that the most burdensome part of the whole loop is parsing ISO8601 dates that we got from database, literally moment(ISO8601_DATE_HERE). It was quite shocking since we weren't using any custom format, just typical ISO. It became even more shocking when we noticed that moment(new Date(ISO8601_DATE_HERE)) is actually faster... about 7 times faster. Wait, what?

Source: Imgur

After that we decided to do more Moment.js performance testing and compare it with other solutions. Take a look:

Comparison of time required to perform common operations x 100 000 [s]

ISO 8601 Parsing

It took Moment.js almost 9 seconds to parse 100 000 ISO 8601 dates, while Day.js needed only 0.5 second. They have similiar API, but under the hood they work differently. Day.js uses smart trick. It detects if there is Z at the end of passed String. If there is, it just uses Native new Date(String). On the other hand Moment.js, Luxon and JS-Joda use their own regex solution. Worth noticing fact is that Date.parse handles ISO 8601 properly since ES5 so if you need to support e.g IE9 (I hope you'll never have to ๐Ÿ˜ฌ) you should probably avoid both Day.js and Date-Fns.

EPOCH Time Parsing

No surprises here. All libraries deal with it pretty nicely, but if you are a speed demon, Date-Fns is your friend.

Formatting

Since there is no actual format function in Date API, even native-based libraries like Date-Fns end with more or less complex custom solutions. First that comes to our mind is probably regex and that's okay, since it's a valid and commonly used solution, for example, by Moment.js. However, the most efficient library in this test, JS-Joda, does it differently. It uses custom function with a lot of ifs and charAts and it seems to be faster than regex-based solutions.

Math

DateTime Math is really tough thing, there is no doubt about it, and in this case Moment.js did really well, unlike Day.js and Luxon. However, again JS-Joda appears to be the winner of math competition.

Comparisons

It's clearly visible that checking if both dates are of the same day is more complex that just checking if first date was before the second. It's because it can't be done just by comparing timestamps.

It's quite strange to me that Luxon did so bad here. We did some basic perf tests inside my chrome dev tools and it looks like most of the time is used by startOf and endOf functions, which seems to be obvious. However, I'm unable to find the reason why they are so slow. It needs deeper digging. Anyway, all the other libraries had pretty fine results.

When it comes to the Is before test, which should be easy, we can notice that Day.js struggles with it. It's because it uses endOf inside its isBefore function and doesn't check if second parameter unit has been passed, unlike Moment, which does it.

2. It's heavy

Moment.js by default weights 232 kB (66 kB gzipped), which - according to the analysis by Yoshihide Jimbo can be reduced to about 68 kB (23 kB gzipped) by ignoring locales using Webpack. It doesn't support tree-shaking because of its design so it seems like we cannot reduce it more.

Comparison of weights [kB]

JS-Joda is slightly lighter than Moment.js (well, gzipped almost 2 times), but we should mention here, that it's a really big library that contains periods and timezones (both not included in basic Moment.js - plugins required).

The difference is even bigger when it comes to Luxon, Day.js and Date-Fns. The last mentioned has tree-shaking support so for most cases it should take much more less space. And last but not least, even without tree-shaking, Day.js weights 3 kB minified & gzipped. That's 22 times lower than Moment.js.

It doesn't really matter if we're talking about Back-End usage, but for sure it should be taken into consideration when it comes to the Front-End. Longer loading time means lower user experience and worse SEO.

3. It's mutable

Let's say that you're building calendar app and you want to create timeframe to fetch incoming events.

const startedAt = moment()
const endedAt   = startedAt.add(1, 'year')

console.log(startedAt) // > 2020-02-09T13:39:07+01:00
console.log(endedAt)   // > 2020-02-09T13:39:07+01:00

All manipulation methods both mutate and return reference to mutated object, which seems to be more error-prone idea than just mutating, since you are not getting an error. endedAt === undefined would warn you at some point that something is wrong.

const init   = moment()
const add    = init.add(1, 'year')
const sub    = init.subtract(10, 'months')
const start  = init.startOf('year')
const end    = init.endOf('year')
const utc    = init.utc()
const local  = init.local()
const offset = init.utcOffset(480)

All these variables reference the same object. Fortunately, there is a simple solution.

const startedAt = moment()
const endedAt   = moment(startedAt).add(1, 'year')

Passing Moment.js object as an argument to moment() method creates new instance. Keep that in mind every time you're using Moment.js!

4. It's hard to debug

If input data is good, everything is fully predictable and works nice (skipping situations in which we forgot about mutating functions, of course). However, sometimes we make mistakes, we are humans after all. It would have been nice to be warned by library that something is not ok with our data.

Let's look at the example. We have an object called person, which has one field - lastVisitedAt. You can think about it as of JSON returned by server API.

const person = { lastVisitedAt: '2017-11-11T00:00:00.000Z' }
moment(person.lastVsitedAt).format() // > '2019-02-08T16:01:45+01:00'

Did you notice that last visit date of our person is different than date returned by Moment.js? Why? Well, I made a typo. It should be lastV**i**sitedAt. By its design, moment(undefined) does not throw an error. It behaves like moment() instead.

Let's check more values.

moment().format()          // > 2019-02-08T17:07:22+01:00
moment(undefined).format() // > 2019-02-08T17:07:22+01:00
moment(null).format()      // > Invalid date
moment({}).format()        // > 2019-02-08T17:07:22+01:00
moment("").format()        // > Invalid date
moment([]).format()        // > 2019-02-08T17:07:22+01:00
moment(NaN).format()       // > Invalid date
moment(0).format()         // > 1970-01-01T01:00:00+01:00

It looks like only NULL, empty string and NaN are invalid. Quite inconsistent. Moreover, no error is thrown, instead Moment.js returns Invalid Date object (which is an instance of a Date btw.).

moment().toISOString()          // >  2019-02-08T16:14:10.835Z
moment(undefined).toISOString() // >  2019-02-08T16:14:10.835Z
moment(null).toISOString()      // >  null
moment({}).toISOString()        // >  2019-02-08T16:14:10.836Z
moment("").toISOString()        // >  null
moment([]).toISOString()        // >  2019-02-08T16:14:10.836Z
moment(NaN).toISOString()       // >  null
moment(0).toISOString()         // >  1970-01-01T00:00:00.000Z

When using toISOString() Moment.js behaves differently. Instead of Invalid Date we get null. Under the hood, Moment.js creates its own invalid object and apparently these methods treat it differently.

moment()          // >  moment("2019-02-08T17:21:46.584")
moment(undefined) // >  moment("2019-02-08T17:21:46.584")
moment(null)      // >  moment.invalid(/* NaN */)
moment({})        // >  moment("2019-02-08T17:21:46.584")
moment("")        // >  moment.invalid(/* NaN */)
moment([])        // >  moment("2019-02-08T17:21:46.584")
moment(NaN)       // >  moment.invalid(/* NaN */)
moment(0)         // >  moment("1970-01-01T01:00:00.000")

To summarize: Undefined is not invalid attribute for moment() function, but null is. Nevertheless, Moment.js won't throw you an error even if you'll pass null. Instead, you'll get native Invalid Date object, null or custom object, depends on the situation. ๐Ÿคฏ

On the other hand...

...Moment.js has a lot of advantages that we can't omit. It has a big community that leads to fast bug detection and fixing. Moreover, you can find a lot of external libraries that add various functionalities (e.g moment-business-days). Another thing is wide timezone support, which is better than in other DateTime libraries.

Alternatives

Upgrading from native Date API to Moment.js was a big improvement, there is no doubt about it, but does it mean it can't be better? Of course not, but hey... what actually does it mean "better"? Well, that depends on your needs.

If size is crucial I'd recommend to try Date-Fns or Day.js. For Back-End and projects that do a lot of error-prone parsings and/or manipulations Luxon or JS-Joda seem to be best choices. If wide support and a lot of plugins are things that you need, stick with Moment.js, but be aware of its issues!

Size (gzip) [kB] To. Speed [s] Tree-shaking Immutable Error throwing TZ Support
Moment.js 232 (66) or 68 (26) 16.527 โŒ โŒ โŒ โœ…
Day.js 6 (3) 9.219 โŒ โœ… โŒ โŒ [1]
Luxon 64 (18) 15.406 โŒ โœ… โœ… [2] โœ… [3]
JS-Joda 208 (39) 11.397 โŒ โœ… โœ… โœ…
Date-Fns 30 (7) 5.175 โœ… โœ… โŒ โŒ
Native Date - 1.297 - โŒ โŒ โŒ

If such a comparison is not enough for you, take a look on basic code example (or run it online! ). I've sorted them by similarities to moment.js API.

const moment                                 = require('moment');
const dayjs                                  = require('dayjs')
const { DateTime }                           = require('luxon')
const { ZonedDateTime, DateTimeFormatter }   = require('js-joda')
const { parse, addYears, subMonths, format } = require('date-fns')

const iso = '2011-10-11T13:00:00.000Z';

// Moment
const from    = moment(iso)
const to      = moment(from).add(1, 'year').subtract(6, 'months')
const format  = 'YYYY-MM-DD [at] HH:mm'
const fromStr = from.format(format)
const toStr   = to.format(format)
const str     = `From ${fromStr} to ${toStr}`
console.log(str) // > From 2011-10-11 at 13:00 to 2012-04-11 at 13:00

// Day.js
const from    = dayjs(iso)
const to      = from.add(1, 'year').subtract(6, 'months')
const format  = 'YYYY-MM-DD [at] HH:mm'
const fromStr = from.format(format)
const toStr   = to.format(format)
const str     = `From ${fromStr} to ${toStr}`
console.log(str) // > From 2011-10-11 at 13:00 to 2012-04-11 at 13:00

// Luxon
const from    = DateTime.fromISO(iso)
const to      = from.plus({ year: 1 }).minus({ month: 6 })
const format  = "yyyy-MM-dd 'at' HH:mm"
const fromStr = from.toFormat(format)
const toStr   = to.toFormat(format)
const str     = `From ${fromStr} to ${toStr}`
console.log(str) // > From 2011-10-11 at 13:00 to 2012-04-11 at 13:00

// JS-Joda
const from    = ZonedDateTime.parse(iso)
const to      = from.plusYears(1).minusMonths(6)
const format  = DateTimeFormatter.ofPattern("y-MM-d 'at' H:mm")
const fromStr = from.format(format)
const toStr   = to.format(format)
const str     = `From ${fromStr} to ${toStr}`
console.log(str) // > From 2011-10-11 at 13:00 to 2012-04-11 at 13:00

// Date-Fns
const from    = parse(iso)
const to      = subMonths(addYears(from, 1), 6) // or you can use any chain tool, e.g @inventistudio/using-js
const formatS = "YYYY-MM-DD [at] HH:mm"
const fromStr = format(from, formatS)
const toStr   = format(to, formatS)
const str     = `From ${fromStr} to ${toStr}`
console.log(str) // > From 2011-10-11 at 13:00 to 2012-04-11 at 13:00

Of course, we don't take into consideration a lot of things that can do the difference, e.g license or popularity, however we believe that amount of informations is a good start!

TL;DR

Moment.js is heavy, slow, mutable and hard to debug, still yet it has some advantages. However you should consider using different library, e.g JS-Joda, Luxon, Date-Fns or Day.js, depending on your needs. And even if you decide to stick with Moment.js, be aware of few things, e.g moment(undefined) will give you valid date.


  1. There is pull request adding TimeZone plugin โ†ฉ๏ธŽ

  2. It requires Settings.throwOnInvalid = true; โ†ฉ๏ธŽ

  3. It may need a polyfill to work in old browsers without Intl API support โ†ฉ๏ธŽ