The Main Thread Is Not Yours
Source: Dev.to
The Main Thread Is Your User’s Resource
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, the direct link between your code and the person using it.
As developers we often use it without considering the end‑user’s device—anything from a mid‑range phone to a high‑end gaming rig. The main thread doesn’t belong to us; it belongs to them.
“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,
- acknowledging a keystroke.
When your code runs long, you’re not causing “jank” in some abstract 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,
- keystrokes pile up hoping you’ll finish soon.
If your code takes 50 ms to respond, nobody notices. At 500 ms the interface feels sluggish, and after several seconds the browser may offer to kill your page entirely.
Users don’t see your code executing; they just see a broken experience and blame themselves, then the browser, then you.
Human Perception & the 200 ms Budget
Browser vendors have spent years studying how humans perceive responsiveness. The research converged on a clear threshold:
| Latency | Perceived Experience |
|---|---|
| ≤ 100 ms | Instant |
| 100 – 200 ms | Noticeable delay |
| > 200 ms | Poor |
The industry formalized this as the Interaction to Next Paint (INP) metric—anything over 200 ms is considered poor and now influences search rankings.
That 200 ms budget isn’t just for your JavaScript. The browser also needs time for style calculations, layout, and painting, so your code gets what’s left—roughly ≈ 50 ms per interaction before the experience starts to feel sluggish. That’s the allocation you have from a resource you don’t own.
APIs That Help You Be a Good Guest
The web platform has evolved to help you stay off the main thread. Many of these APIs exist because browser engineers got tired of watching developers block the thread unnecessarily.
Web Workers
Run JavaScript in a completely separate thread. Heavy computation—parsing large datasets, image processing, 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; it forces a clean separation between “work” and “interaction.”
requestIdleCallback
Run code only when the browser has nothing better to do. (Due to a WebKit bug, Safari support is still pending at the time of writing.) When the user is actively interacting, your callback waits; when things are quiet, your code gets a turn.
requestIdleCallback((deadline) => {
// Process 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 finish later.
if (tasks.length) {
requestIdleCallback(processRemainingTasks);
}
});
Ideal for non‑urgent work like analytics, pre‑fetching, or background updates.
navigator.scheduling.isInputPending
(Chromium‑only for now.) This API 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.
Subtle Main‑Thread Crimes
The obvious crimes—infinite loops, rendering 100 000 table rows—are easy to spot. The subtle ones look harmless.
JSON.parse()on a large API response blocks the main thread until parsing completes. On a developer’s machine it feels instant; on a mid‑range phone with a throttled CPU and competing tabs it might take 300 ms, 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.
Measuring What You Can’t Manage
You can’t manage what you can’t measure. Chrome DevTools’ Performance panel shows exactly where your main‑thread time goes—if you know where to look.
- Open the Performance panel and record a session.
- Find the “Main” track.
- Look for long yellow blocks of JavaScript execution.
- Tasks exceeding 50 ms are flagged with red shading to mark the overtime portion.
If you prefer a guided approach, use the Insights pane to surface long tasks automatically.
performance.measure() for Precise Instrumentation
// 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');
// Measure the duration
performance.measure('JSON parse', 'parse-start', 'parse-end');
Now you can see the exact time spent on that operation in the Performance panel.
Takeaway
Treat the main thread as your user’s limited budget.
Use Workers, requestIdleCallback, and isInputPending to keep the UI responsive, avoid hidden blocking calls, and always measure your impact. When you respect that budget, the experience feels instant, users stay happy, and your site performs better in the real world—and in search rankings.
// Measure 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; when you see spikes, you’ll know where to start investigating.
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 200 ms 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 – resumability avoids hydration entirely.
- React – concurrent features let rendering yield to user input.
These are all responses to the same fundamental constraint: the main thread is finite, and we’ve been spending it carelessly. The technical solutions matter, but they follow from a shift in perspective. 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.