On Number Formatting, and Performance
This post is referencing the development of gw2efficiency, my current main side project. If you have never used the site, it is a helper application for the MMORPG Guild Wars 2, which allows you to manage your account, generate and compare statistics with other players and calculate the best ways to optimize your play-style.
Today I was finalizing some internal changes on how the frontend components that resemble the "building blocks" making up gw2efficiency are developed. As part of the migration, I did some manual tests to make sure everything still worked and felt the same as before, with no performance degradations.
To my surprise, clicking a table header, which causes the table to reorder, felt very sluggish. I opened up the performance tab in Chrome, did a quick recording, and was a bit annoyed to see the click handler taking over half a second to execute. This meant that from the moment the mouse got released to the moment the table finished rendering the new state, over half a second passed, which was unacceptably slow.
As the first course of action, I checked how long the sorting of the table rows took, since that was arguably the most computationally expensive operation, but the result of 5 milliseconds showed the culprit was somewhere else.
Surely, a table with just a bunch of text and numbers should not take half a second to render. In an attempt to narrow down the issue, I opened up the profiler in the React Developer Tools, which showed the child components the table was built out of and their render times.
In the profiler, the culprit was immediately clear. NumberFormat (0.9ms)
. I re-read it in disbelief. This component's job is (as the name suggests) to format numbers. Essentially, it turns a number like 57452
into 57,452
for the English-speaking users, into 57.452
for the German-speaking users, and so on.
On the inside, it uses the Intl API, which is provided by modern browsers. I whipped up a small test script and copy & pasted the implementation of the component, to confirm the theory in isolation.
const start = performance.now()
Intl.NumberFormat('de').format(1051285)
const end = performance.now()
console.log(end - start)
// -> 0.9500002488493919 ms
Formatting a single number took 1 millisecond. On first glance, this may not seem like a lot, but I was consistently rendering multiple hundred numbers per page, and this could easily turn into half a second delays that felt terrible for the user.
I quickly typed "Intl number format performance" into Google and examined the first search result, an issue from October 2014. It turns out that creating instances of the number formatter was extremely slow, but formatting numbers was quite fast, as you would expect from such a simple operation. I edited my test script and ran it again.
const formatter = Intl.NumberFormat('de')
const start = performance.now()
formatter.format(1051285)
const end = performance.now()
console.log(end - start)
// -> 0.014999997802078724 ms
The result verified what was mentioned in the issue. Formatting the number was much faster than creating the formatter. Now intrigued, I whipped up a small benchmark on JsPerf, and the results made me stare at the screen for a few seconds. Reusing the formatter was a 724x performance increase.
A small change in the underlying component to reuse the formatter for the same options now meant that in the next release, I could happily render numbers at the performance I expected, and the rendering of a few hundred numbers did not lag the browser anymore.
Addendum: After writing this post, I tried to recreate my results but was surprised to see that the initial benchmark in JsPerf now showed much better performance, by around 25x. I am unsure what caused this change but if I had to guess I'd assume a browser update. That said, reusing the formatter is still around 28x faster than recreating it.