Skip to content

Yoyo Code
Matyáš Racek's blog

2 Basic rules of performance-aware JavaScript in the browser

Over time, I've developed some intuition for writing JavaScript on the frontend with performance in mind. This is not really about optimization, it's more about using reasonable defaults. Almost all of it boils down to just two rules of thumb:

1. The number one cost is JavaScript itself 🔗

Forget all the jsPerf benchmarks. It doesn't matter if you use 'key' in obj or hasOwnProperty. Or more specifically, it shouldn't matter. If it matters, you have a bigger problem.

The biggest cost often comes from the JavaScript code just existing in the first place. From all the resource types on the web, JavaScript is by far the heaviest. This means that if you can do something without JavaScript, it's usually better to do it without JavaScript.

Some implications for the JavaScript code itself:

  • Avoid putting data in JavaScript code (hardcoded strings, big object literals, html templates or jsx (yes, I mean this)).
    • Put it elsewhere instead and let the JavaScript code just process it, if necessary
  • Use very generic pieces of code, that you attach to relevant places (htmx is an extreme example for HTML)
    • e.g., Instead of submitLoginForm, use generic submitForm and attach it to all forms, handle their differences in HTML and on the backend
  • Write code that can be size-optimized. This is a big topic, but the basic idea is to prefer variables over objects, free functions over methods, module pattern over classes, and generally avoid naming long object properties.
  • A good guideline is that the amount of code should grow slower than the size of your app (less than O(n) where n is size of your app). If you have to add a new JavaScript file with a big render function for each page of your app, it will be challenging to keep the amount of code in check.

Summarized, you want your code to be mostly data driven, dense and generic. Note that I don't think this is necessarily good advice for code in general. We just have to make some trade-offs here.

2. Don't do the work in JavaScript 🔗

The key thing to understand is the role of JavaScript in the system. JavaScript is not good at doing actual work. It's interpreted, loosely typed and costly to process. The workhorse of the web is the browser. JavaScript works the best when it's used only as a plumbing for processes that happen in native code.

Practically speaking, you want to look for approaches where the ratio of JavaScript to native code executed is very low. A great example is fetch API. A typical usage looks something like this:

  .then(response => response.json())

Notice how this code does very little in the JS land, but does a ton of work in native land. It has to create a TCP connection, HTTP connection, create and send the request, receive response and parse it as JSON (which is done in a background thread to unblock the main thread). That's a pretty good work ratio.

Simpler examples include lookup APIs like getElementById and querySelector, where walking the DOM tree would be expensive in JavaScript, or style modifications like element.classList.add('bg-green') which trigger a lot of native code to redraw the screen correctly. Of course, things get more complicated in aggregate, but you get the idea.

Also remember, it's not only that the JS is slow, the bigger problem is that it's blocking the main thread. That's why it's often worth delegating the work elsewhere, even when the other party is slower than the JavaScript. By doing that, you're unblocking other processes that compete over the main thread.

A good moment to pause and think about this is when you write some loop or loop-like construct ( etc.). There's nothing wrong with a loop, but it might indicate that you're trying to do some actual work in JavaScript. Think if you can offload some of that work to the browser or do it on the backend and send the results to the frontend instead.

Conclusion 🔗

You might notice that it can be quite challenging to follow these guidelines with popular frontend frameworks these days. That's right, modern JavaScript frameworks are not as good with this and not only that, they are built on some fundamental assumptions that make it difficult to follow these guidelines properly. That's why I believe we have to find something better. I'll write more about this in the future.

It's quite funny that these rules for performant JavaScript are basically just to try to avoid it in various ways, but I think that's a good mindset.