← Writing

The Main Thread Is a Queueing Problem

A practical model for understanding why browser work feels instant until one task holds the main thread for too long.

On this pageLong tasks hide the queue

The browser’s main thread has an awkward job. It runs JavaScript, calculates styles, performs layout, and helps prepare pixels for the screen. Most of the time these jobs are short enough that the arrangement feels invisible. A click happens, its handler runs, and the page changes.

The useful mental model is a single desk with an inbox. Work can arrive from timers, network responses, user input, or code that is already running. The desk handles one task at a time. A fast computer can clear the inbox quickly, but it cannot work on two main-thread tasks at once.

Long tasks hide the queue#

A task that runs for 120 milliseconds does more than consume 120 milliseconds of CPU time. It also makes everything behind it wait. A pointer event may already be ready. The browser may need to paint a pressed button. Neither can begin until the current task yields.

button.addEventListener('click', () => {
  const startedAt = performance.now();

  while (performance.now() - startedAt < 120) {
    // Deliberately hold the main thread.
  }
});

This is why performance work often starts with task duration rather than total computation. Breaking a large unit of work into smaller pieces gives the browser chances to handle input and render between them. The same amount of calculation can feel very different when it stops monopolizing the desk.

What to inspect#

When an interaction feels sticky, record it in the browser’s performance tools. Look for a task that spans the delay, then inspect the call stack inside it. The queueing model turns a vague complaint into a concrete question: what occupied the main thread while the user was waiting?