A major traffic jam in New York City, denoting the blocking of the browser's main thread by excess JavaScript execution.

The Main Thread Is Not Yours

Den Odell

Den Odell 8 January 2026 · ⏱️ 7 min read

When a user visits your site or app, their browser dedicates a single thread to running your JavaScript, handling their interactions, and painting what they see on the screen. This is the main thread, and it’s the direct link between your code and the person using it.

As developers, we often use it without considering the end user and their device, which could be anything from a mid-range phone to a high-end gaming rig. We don’t think about the fact that the main thread doesn’t belong to us; it belongs to them.

I’ve watched this mistake get repeated for years: we burn through the user’s main thread budget as if it were free, and then act surprised when the interface feels broken.


Every millisecond you spend executing JavaScript is a millisecond the browser can’t spend responding to a click, updating a scroll position, or acknowledging that the user did just try to type something. When your code runs long, you’re not causing “jank” in some abstract technical sense; you’re ignoring someone who’s trying to talk to you.

Because the main thread can only do one thing at a time, everything else waits while your JavaScript executes: clicks queue up, scrolls freeze, and keystrokes pile up hoping you’ll finish soon. If your code takes 50ms to respond nobody notices, but at 500ms the interface starts feeling sluggish, and after several seconds the browser may offer to kill your page entirely.

Users don’t know why the interface isn’t responding. They don’t see your code executing; they just see a broken experience and blame themselves, then the browser, then you, in that order.

The 200ms You Don’t Own

Browser vendors have spent years studying how humans perceive responsiveness, and the research converged on a threshold: respond to user input within 100ms and the interaction feels instant, push past 200ms and users notice the delay. The industry formalized this as the Interaction to Next Paint (INP) metric, where anything over 200ms is considered poor and now affects your search rankings.

But that 200ms budget isn’t just for your JavaScript. The browser needs time for style calculations, layout, and painting, so your code gets what’s left: maybe 50ms per interaction before things start feeling wrong. That’s your allocation from a resource you don’t own.

The Platform Has Your Back

The web platform has evolved specifically to help you be a better guest on the main thread, and many of these APIs exist because browser engineers got tired of watching developers block the thread unnecessarily.

Web Workers let you run JavaScript in a completely separate thread. Heavy computation, whether parsing large datasets, image processing, or complex calculations, can happen in a Worker without blocking the main thread at all:

// Main thread: delegate work and stay responsive
const worker = new Worker('heavy-lifting.js');

// Send a large dataset from the main thread to the worker
// The worker then processes it in its own thread
worker.postMessage(largeDataset);

// Receive results back and update the UI
worker.onmessage = (e) => updateUI(e.data);

Workers can’t touch the DOM, but that constraint is deliberate since it forces a clean separation between “work” and “interaction.”

requestIdleCallback lets you run code only when the browser has nothing better to do. (Due to a WebKit bug, Safari support is still pending at time of writing.) When the user is actively interacting, your callback waits; when things are quiet, your code gets a turn:

requestIdleCallback((deadline) => {

  // Process your tasks from a queue you created earlier
  // deadline.timeRemaining() tells you how much time you have left
  while (tasks.length && deadline.timeRemaining() > 0) {
    processTask(tasks.shift());
  }

  // If there are tasks left, schedule another idle callback to complete later
  if (tasks.length) requestIdleCallback(processRemainingTasks);
});

This is ideal for non-urgent work like analytics, pre-fetching, or background updates.

isInputPending (Chromium-only for now) is perhaps the most user-respecting API of the lot, because it lets you check mid-task whether someone is waiting for you:

function processChunk(items) {

  // Process items from a queue one at a time
  while (items.length) {
    processItem(items.shift());

    // Check if there’s pending input from the user
    if (navigator.scheduling?.isInputPending()) {

      // Yield to the main thread to handle user input,
      // then resume processing after
      setTimeout(() => processChunk(items), 0);

      // Stop processing for now
      return;
    }
  }
}

You’re explicitly asking “is someone trying to get my attention?” and if the answer is yes, you stop and let them.

The Subtle Offenders

The obvious main thread crimes like infinite loops or rendering 100,000 table rows are easy to spot, but the subtle ones look harmless.

Calling JSON.parse(), for example, on a large API response blocks the main thread until parsing completes, and while this feels instant on a developer’s machine, a mid-range phone with a throttled CPU and competing browser tabs might take 300ms to finish the same operation, ignoring the user’s interactions the whole time.

The main thread doesn’t degrade gracefully; it’s either responsive or it isn’t, and your users are running your code in conditions you’ve probably never tested.

Don’t miss the next post.

Behind the scenes on web platform features, architecture, and performance.
Monthly. No spam.

Read by developers at Google, Microsoft, and Stripe.

Measure What You Spend

You can’t manage what you can’t measure, and Chrome DevTools’ Performance panel shows exactly where your main thread time goes if you know where to look. Find the “Main” track and watch for long yellow blocks of JavaScript execution. Tasks exceeding 50ms get flagged with red shading to mark the overtime portion. Use the Insights pane to surface these automatically if you prefer a guided approach. For more precise instrumentation, the performance.measure() API lets you time specific operations in your own code:

// Mark the start of a heavy operation
performance.mark('parse-start');

// The operation you want to measure
const data = JSON.parse(hugePayload);

// Mark the end of the operation
performance.mark('parse-end');

// Name the measurement for later analysis
performance.measure('json-parse', 'parse-start', 'parse-end');

The Web Vitals library can capture INP scores from real users across all major browsers in production, and when you see spikes you’ll know where to start investigating.

The Framework Tax

Before your application code runs a single line, your framework has already spent some of the user’s main thread budget on initialization, hydration, and virtual DOM reconciliation.

This isn’t an argument against frameworks so much as an argument for understanding what you’re spending. A framework that costs 200ms to hydrate has consumed four times your per-interaction budget before you’ve done anything, and that needs to be a conscious choice you’re making, rather than an accident.

Some frameworks have started taking this seriously: Qwik’s “resumability” avoids hydration entirely, while React’s concurrent features let rendering yield to user input. These are all responses to the same fundamental constraint, which is that the main thread is finite and we’ve been spending it carelessly.

Borrow, Don’t Take

The technical solutions matter, but they follow from a shift in perspective, and when I finally internalized that the main thread belongs to the user, not to me, my own decisions started to change.

Performance stops being about how fast your code executes and starts being about how responsive the interface stays while your code executes. Blocking the main thread stops being an implementation detail and starts feeling like taking something that isn’t yours.

The browser gave us a single thread of execution, and it gave our users that same thread for interacting with what we built. The least we can do is share it fairly.


💬 Join the discussion

Continue the conversation on your favourite community.

🔗 Share this post

Spread the word wherever you hang out online.

Twitter/X LinkedIn

Related Posts

Enjoyed this? Subscribe for the behind the scenes.

Frontend engineering, technical decisions, web-native patterns. Monthly.

Join developers from Google, Microsoft, and Stripe.