Moment.js, a widely recognized date manipulation library, has been in legacy mode for almost three years now, starting from September 2020. However, it’s still slightly more used than its popular competitors such as dayjs and date-fns, at least according to npm stats (~17M weekly downloads vs ~14M for each of the competitors). Hence, there is a decent chance that you, my dear reader, are using it too.
Moment.js is in maintenance mode due to a variety of reasons. Some of them were described by the authors themselves, and some are well known to the community such as the notorious “why the hell are the moment locales in my bundle and how do I remove them”. Also, a lot of people have encountered issues with slow functions like isSameOrBefore().
However, there’s definitely more!
Let’s say, we fetch an array of Employee objects that, among other things, contains several stringified dates in DD-MM-YYYY format:
const sampleEmployee: Employee = {
versionId: '01-01-2023',
startDate: '01-01-2020',
termDate: '01-01-2024'
};
Now we might want to do something with those dates. Like, in this case, calculate the length of the contract. Or anything else. Whatever.
The thing that matters is that we can’t perform any date calculation with strings, so we need to instantiate a Date object — which is absolutely straightforward.
But if we’re already using Moment.js, there’s a good chance that there is already a bunch of helper functions ( thousands of lines in utils.ts, I’m looking at you!) that we want to use, for example, to properly handle date granularity. Or at least for consistency.
Now, we aren’t dealing with just one object, we have an array of them. Regardless of our task, if there is any date logic, we’re going to create a moment object on every iteration of the loop, probably with a helper function of some sort, but in this example let’s use moment directly for simplicity.
We know that moment constructor is really smart and can instantiate a valid object from pretty much everything that resembles a date, so the code would look like this:
const mock = { versionId: '01-01-2020', termDate: '02-01-2021', startDate: '03-01-2020' };
const mockData = Array(10000).fill(mock);
const result = mockData.map(item => {
const versionId = moment(item.versionId);
const startDate = moment(item.startDate);
const termDate = moment(item.termDate);
// It doesn't really matter what we do with the dates. Just return them:
return { versionId, startDate, termDate };
});
Let’s test this code on a modern laptop, but with 4x CPU slowdown to emulate a slower device:
We spend a good 1/3 of a second iterating through the loop and creating 3-moment objects on each iteration. We didn’t even calculate anything in this loop yet, but it’s already fairly slow. But why?
It turns out that the Moment constructor is slower when it’s using a string date instead of a Date object!
I have created a benchmark to prove this hypothesis, and it shows that even with the overhead of creating a native Date object, it’s almost twice as fast in comparison with the direct usage of a string:
Let’s test it directly with the CPU slowdown:
const result = mockData.map(item => {
const versionId = moment(new Date(item.versionId));
const startDate = moment(new Date(item.startDate));
const termDate = moment(new Date(item.termDate));
return { versionId, startDate, termDate };
});
Now, it’s understandable that there might be a lot of logic in the codebase that’s already written using moment.js.
There might be no development resources to replace it right away. If this is the case, this is already a good improvement. However, if there is a possibility to replace it, the speed can be improved even more if a native Date is used.
Yes, the native Date API is still clunky. And here is where date-fns truly shines, because it’s using native Date objects. Therefore, if we need to do something with our dates such as to calculate the difference, no custom objects will be created, saving the runtime.
Let’s run the test again, but this time we’ll only create the Date objects:
const result = mockData.map(item => {
const versionId = new Date(item.versionId);
const startDate = new Date(item.startDate);
const termDate = new Date(item.termDate);
return { versionId, startDate, termDate };
});
Now this is three times faster than the original version. Quite nice, isn’t it?
A small post-scriptum: when working with dates in the loops, it’s sometimes a really good idea to memoize utility functions. It depends on the data you’re dealing with, but there is a good chance that the dates might be repeated over and over in different objects. Therefore, if there is a way to not repeat the calculations — take advantage of it, it might save few more precious milliseconds, resulting in an improved user experience.
(Yet) Another Way to Harm Performance With Moment.js was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.