As part of my developer workflow, I check for performance problems regularly, especially on important pages.
Naturally, I wanted to take a further look. I soon discovered an important page was taking six seconds to load – way over what we aim for (less than two seconds is our threshold). After making some improvements and running some tests, I reduced the page load speed from six seconds to 600 milliseconds. That’s a 1000% improvement.
In this post, I wanted to share with you how I detected these issues, how I diagnosed the problem, and how I found a fix. I also wanted to show you the maintenance of this fix by running some performance tests – which you can replicate if you’d like.
How I found the problem
My daily check usually involves looking at the dashboard of our Real User Monitoring software which picks up on performance issues, so it was quite easy for me to see there was a problem.
Drilling down, I could see render time was spending a majority of the load.
A slow render indicates that something is halting the render of the page. Often, you can attribute a slow render to a slow script execution on, or before, page render.
So, it was an issue with one of our scripts. The best way to validate a slow script is to do some local testing, and Chrome Dev Tools is a great place to profile the script.
Chrome Dev Tools showed a definite bottleneck with a script for one of our charts. Two method calls inside the chart script were causing a bottleneck, one was a ‘filter’ method, and the other was a ‘bucket’ method.
Drilling down further, a majority of the time spent in each of these methods was made up of calls to the Moment.js library. The chart in question is interesting because it fetches all of its data immediately and handles any filtering and bucketing of the data locally. So, the script has to carry out a large number of datetime comparisons to get a displayable set of data points. From this, it would appear that there is quite a significant performance overhead with using the Moment library.
To fix this issue, I stripped out Moment for Native dates in the underperforming methods. Although I couldn’t completely remove Moment, as it’s formatting methods are crucial to our date-time representations, I still saw a significant performance boost.
This change alone increased the filtering time of this data from around 6 seconds to 600 milliseconds.
Why I decided to test for performance
After a few months, I wanted to ensure the improvements were still making a positive impact in Raygun’s application. Making any significant changes in your application warrants a check-in after a few months. I tend to look for the following:
I also wanted to consider that another library could replace Moment and still offer us date formatting functionality.
The test between Moment.js vs date-fns
I conducted testing against both Moment.js, date-fns and the native date methods where available. For testing, I focused on the collection of dates rather than individual dates, as this was my primary use case and I was also interested in testing the sort speed of the different libraries.
If you’d like to reproduce the test, the code is on GitHub here.
Here are the results.
Data set for small, medium and large data sets
Firstly, results show a significant overhead associated with the creation of Moment objects. As you can see from the above tests, the creation speed of a Moment can range from 7x to 17x the creation speed of a native JS Date object. Date-fns does not suffer from such overheads as it utilizes JS Dates instead proving a wrapper like Moment.js.
var moment = moment();
var nativeDate = new Date();
One thing Moment seems to excel at, according to the above tests, is formatting. Moment can create formatted date strings nearly twice as fast as date-fns, and the JS Date library doesn’t even support this functionality. Quick formatting is a big plus for Moment as it’s the most useful feature in a front end date time library.
var formattedMoment = moment().format("dddd, MMMM Do YYYY, h:mm:ss a");
var formattedDate = fns.format(new Date(), "dddd, MMMM Do YYYY, h:mm:ss a");
One thing I found interesting with Moment was it lacks performance in seemingly inexpensive functions. I included one such function in my results above, this being the ‘From Now’ calculation. The purpose of this function is to retrieve the duration as a formatted string (e.g. one minute ago), between the specified datetime and the current datetime. The length of this function call ballooned when compared with the equivalent function in date-fns.
var fromNow = moment().fromNow();
var fromNow = fns.distanceInWordsToNow(new Date());
My take on the results
All in all, the results are somewhat as I expected. Moment appears to have a significant performance overhead in many areas due to its complex API. The fact that a Moment object takes so long to instantiate is quite an off putting factor for me. This overhead will always be absorbed even if you aren’t getting extra functionality from the library.
It is worth noting that I had a slight bias toward using another tool as Moment is an impure library, and could be creating more bugs. In saying this, however, I still think Moment has its place. There are still many areas in which the functionality if of a library like date-fns cannot contend with Moment, including locales, timezones, and durations.
I found Moment has quite a big performance overhead. Therefore, it would be more beneficial for us to use a solution that is an extension to the language, rather than a tool with a bulky API like Moment. We don’t require all the features Moment offers, so it makes sense to use something more lightweight. You might be different, of course.
Something of note is that the duration scale remained consistent throughout the all three data sets – small, medium and large. So, it doesn’t matter how large your data set is.
Six seconds to 600ms is an impressive performance improvement and on the extreme end of the scale. If you suspect there’s a problem with a script or elsewhere, go and investigate – you never know where performance problems are lurking! Thankfully, RUM made the discovery process much easier, and I may never have found the weak script.