HeadlinesBriefing favicon HeadlinesBriefing.com

Browser Main Thread Performance Explained

Hacker News •
×

What comes to mind when you hear “frontend optimization”? For most of us it’s things like reducing network requests, shrinking the bundle, or making good use of the cache. Beyond that, maybe cutting down on re-renders or tuning when resources get loaded. The main thread doesn’t usually come up, and there’s a reason for that: on most screens it never becomes a problem.

But on screens with a lot of interaction, where data streams in live and scrolling, animation, and input all get tangled together, the picture changes. However much you save on network and bundle size, the screen freezes the moment the main thread gets blocked. You’ve probably come across a website where scrolling stutters now and then, a button responds slightly late, or the letters you type into a search box show up half a beat behind.

It isn’t bad enough to be annoying, but it gets on your nerves in a subtle way. That kind of jank is what a blocked main thread looks like. When we run into jank like this as developers, the usual reaction is to wonder “is my code slow?” and start picking apart algorithms or looking for wasted computation.

In most cases, though, the speed of the code is not the problem. The code isn’t slow. It just happens to be the code that’s holding the main thread.

The browser has a number of threads, but almost everything we can touch from code is concentrated on the main thread. Computation, rendering, event handling, network response handling, and your framework’s internals are all processed there. One resource, a mountain of work.

The browser’s main thread is expensive. Most of the time it doesn’t cause trouble, but once you try to do something ambitious, dealing with the main thread becomes the important part. This article is about how to handle that expensive resource.

Let’s start with what the main thread actually does. Its work falls into two broad categories. The first is running Java Script.

The code we write, along with event handlers, timers, network response callbacks, and the framework’s internals, all run here. These tasks execute in the order they enter the queue, whenever there is a gap, with no relation to the screen refresh cycle. The second is drawing the screen.

When the DOM or styles change and the screen needs updating, the browser goes through roughly these steps, in order, to produce a frame. Run request Animation Frame callbacks - Java Script registered to run just before the frame is drawn Style calculation - compute the final CSS values for each element Layout - compute each element’s position and size (also called reflow)Paint - generate paint commands describing what to draw in which colors If nothing changed, these steps are skipped entirely, so they don’t necessarily run every frame. Only the final compositing step, which takes the produced output and assembles it on screen, is handed off to the compositor thread.

In other words, most of the front half of the pipeline that draws the screen is the main thread’s responsibility. The rendering pipeline for updating the screen For the screen to look smooth, frames have to be drawn at the display’s refresh rate. On the most common 60Hz display, that means 60 frames per second, or about 16.6 milliseconds per frame.

And you don’t get to use all of it. Once the browser’s own processing cost is subtracted, the practical budget is usually considered to be around 10 milliseconds, and on a 120Hz device the budget itself is cut in half. The problem is that the two kinds of work above stand in a single line on the same thread.

Java Script was designed around a single-threaded event loop model. The main thread processes one task at a time, and while that task is running, nothing else can happen. If one Java Script function runs for 200 milliseconds, then for those 200 milliseconds the browser can’t repaint the screen or receive a click from the user.

Against a frame budget of around 10 milliseconds, that is a fatal amount of time. A task that ru...