JavaScript Micro Performance Testing, History, and Limitations
Samuel Rouse
Posted on September 10, 2024
I think performance optimization interests many developers as they learn more about different ways to accomplish a task. Some internal voice asks, "Which way is best?" While there are many shifting metrics for "best", like Douglas Crockford's 2008 JavaScript: The Good Parts, performance is accessible because we can test it ourselves.
However, testing and proving performance are not always easy to get right.
A Bit of History
Browser Wars
By the early 2000s, Internet Explorer had won the first browser wars. IE was even the default browser on Macs for a while. Once-dominant Netscape was sold to AOL and eventually shut down. Their spin-off Mozilla was in a years-long beta for their new standalone browser Phoenix Firebird Firefox.
In 2003 Opera 7 came out with Presto, a new, faster rendering engine. Also Apple released Safari, a performance-focused browser for Macs built on the little-known Konqueror KHTML engine. Firefox officially launched in 2004. Microsoft released IE 7 in 2006, and Opera 9 released a faster JavaScript engine. 2007 brought Safari on both Windows and the new iPhone. 2008 saw Google Chrome and the Android browser.
With more browsers and more platforms, performance was a key part of this period. New browser versions regularly announced they were the new fastest browser. Benchmarks like Apple's SunSpider and Mozilla's Kraken were frequently cited in releases and Google maintained their own Octane test suite. In 2010 the Chrome team even made a series of "speed test" experiments to demonstrate the performance of the browser.
High Performance JavaScript
Micro Performance testing saw a lot of attention in the 2010s. The Web was shifting from limited on-page interactivity to full client-side Single Page Applications. Books like Nicholas Zakas's 2010 High Performance JavaScript demonstrated how seemingly small design choices and coding practices could have meaningful performance impacts.
Constant Change
Before long the JavaScript engine competition was addressing some of these key performance concerns in High Performance JavaScript, and the rapid changes in the engines made it difficult to know what was best right now. With new browser versions and mobile devices all around, micro performance testing was a hot topic. By 2015, the now-closed performance testing site jsperf.com was so popular it started having its own performance issues due to spamming.
Test The Right Thing
With JavaScript engines evolving, it was easy to write tests, but hard to make sure your tests were fair or even valid. If your tests consumed a lot of memory, later tests might see delays from garbage collection. Was setup time counted or excluded from all tests? Were the tests even producing the same output? Did the context of the test matter? If we tested !~arr.indexOf(val)
vs arr.indexOf(val) === -1
did it make a difference if we were just running the expression or consuming it in an if
condition?
Compiler Optimization
As the script interpreters were replaced with various compilers, we started to see some of the benefits — and side-effects — of compiled code: optimizations. Code running in a loop that has no side-effects, for instance, might be optimized out completely.
// Testing the speed of different comparison operators
for (let i = 0; i < 10000; i += 1) {
a === 10;
}
Because this is performing an operation 10000 times with no output or side effects, optimization could discard it completely. It wasn't a guarantee, though.
Moving Targets
Also, micro-optimizations can change significantly from release to release. The unfortunate shuttering of jsperf.com meant millions of historical test comparisons over different browser versions were lost, but this is still something we can see today over time.
It's important to keep in mind that micro-optimization performance testing comes with a lot caveats.
As performance improvements started to level off, we saw test results bounce around. Part of this was improvements in the engines, but we also saw engines optimizing code for common patterns. Even if better-coded solutions existed, there was a real benefit to users in optimizing common code patterns rather than expecting every site to make changes.
Shifting Landscape
Worse than the shifting browser performance, 2018 saw changes to the accuracy and precision of timers to mitigate speculative execution attacks like Spectre and Meltdown. I wrote a separate article about these timing issues, if that interests you.
Split Focus
To complicate matters, do you test and optimize for the latest browser, or your project's lowest supported browser? Similarly, as smartphones gained popularity, handheld devices with significantly less processing power became important considerations. Knowing where to allocate your time for the best results – or most impactful results – became even more difficult.
Premature Optimization?
Premature optimization is the root of all evil.
-- Donald Knuth
This gets quoted frequently. People use it to suggest that whenever we think about optimization, we are probably wasting time and making our code worse for the sake of an imaginary or insignificant gain. This is probably true in many cases. But there is more to the quote:
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.
The more complete quote adds critical context. We can spend a lot of time on small efficiencies if we allow ourselves to do so. This often takes time from the goal of the project without providing much value.
Diminishing Returns
I personally spent a lot of time on these optimizations, and in the moment it didn't seem like a waste. But in retrospect, it's not clear how much of that work was worthwhile. I'm sure some of the code I wrote back then shaved milliseconds off the execution time, but I couldn't really say if the time saved was important.
Google even talks about diminishing returns in their 2017 retirement of the Octane test suite. I strongly recommend reading this post for some great insight into limitations and problems in performance optimization that were experienced by teams dedicated to that work.
So how do we focus on that "critical 3%"?
Application not Operation
Understanding how and when the code is used helps us make better decisions about where to focus.
Tools Not Rules
It wasn't long before the performance increases and variations of new browsers started pushing us away from these kinds of micro-tests and into broader tools like flame charts.
If you have 30 minutes, I recommend this 2015 Chrome DevSummit presentation on the V8 engine. It talks about exactly these issues... that the browsers keep changing, and keeping up with those details can be difficult.
Using performance monitoring and analysis of your running application can help you quickly identify what parts of your code are running slowly or running frequently. This puts you in a great position to look at optimizations.
Focus
Using performance monitoring tools and libraries let you see how the code runs, and which parts need work. They also give us a chance to see if different areas need work on different platforms or browsers. Perhaps localStorage is much slower on a Chromebook with limited memory and eMMC storage. Perhaps you need to cache more information to combat slow or spotty cellular service. We can make guesses at what is wrong, but measuring is a much better solution.
If your customer base is large enough you might find benefit in Real User Monitoring (RUM) tools, that can potentially let you know what the actual customer experience is like. These are outside the scope of this article, but I have used them at several companies to understand the range of customer experience and focus efforts on real-world performance and error handling.
Alternatives
It's easy to dive into "how do I improve this thing", but that isn't always the best answer. You may save a lot of time by stepping back and asking, "Is this the right solution for this problem?"
Issues loading a very large list of elements on the DOM? Maybe a virtualized list where only the visible elements are loaded on the page would resolve the performance issue.
Performing many complex operations on the client? Would it be faster to calculate some or all of this on the server? Can some of the work be cached?
Taking a bigger step back: Is this the right user interface for this task? If you designed a dropdown to expect twenty entries and you now have three thousand, maybe you need a different component or experience for making a selection.
Good Enough?
With any performance work, there is a secondary question of "what is enough"? There's an excellent video from Matt Parker of Stand-up Maths talking about some code he wrote and how his community improved it from weeks of runtime to milliseconds. While it's incredible that such an optimization was possible, there's also a point for nearly all projects at which you reach "good enough".
For a program that is run just once, weeks might be acceptable, hours would be better, but how much time you spend on it rapidly becomes an important consideration.
You might think of it like tolerances in engineering. We have a goal, and we have a range of acceptance. We can aim for perfect while understanding that success and perfection aren't the same.
Identifying Performance Goals
Goals are a critical part of optimization. If you only know that the current state is bad, "make it better" is an open-ended goal. Without a goal for your optimization journey you can waste time trying to find more performance or more optimization when you could work on something more important.
I don't have a good metric for this because performance optimization can vary wildly, but try not to get lost in the weeds. This is really about project management and planning more than coding solutions, but developer input is important when defining optimization goals. As suggested in the Alternatives section, the fix may not be "make this faster".
Setting Limits
In Matt Parker's case, he needed the answer eventually, and didn't need to use the device for anything else. In our world, we're often measuring visitor performance and its probable financial impact vs developer/team time and your opportunity cost, so the measure isn't as simple.
Let's imagine we know reducing our add-to-cart time by 50% would increase our income by 10%, but it will take two months to complete that work. Is there something that could have a bigger financial impact than two months of optimization work? Can you accomplish some benefit in a shorter period of time? Again, this is more about project management than code.
Isolate Complexity
When you do find yourself needing to optimize code, it's also a good time to see if you can separate that code from other parts of your project. If you know you have to write complex optimizations that will make the code difficult to follow, extracting it to a utility or library can make it easier to reuse and allows you to update that optimization in one place if it needs to change over time.
Conclusion
Performance is a complicated subject with lots of twists and turns. If you aren't careful you can put in a lot of energy for very little practical gain. Curiosity can be a good teacher, but it doesn't always achieve results. There is a benefit to playing with code performance, but also a time to analyze actual sources of slowness in your project and use the available tools to help resolve them.
Resources
- Addy Osmani - Visualising JS processing over time with DevTools Flame Charts
- Stand-Up Maths - Someone improved my code by 40,832,277,770%
- Title image made with Microsoft Copilot
Posted on September 10, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024