Main-thread blocking experiment
Run real work on your real main thread and watch your real frame rate fall over. No simulation.
- Level
- ENGINEER
- Read time
- 08 min
- Experiment
- Available
- Type
- MEASUREMENT
The question
What does 'blocking the main thread' feel like, measured rather than described, and which escape hatches actually help?
Hypothesis
Any single task longer than ~50ms is a long task. Above ~200ms, input feels broken. Splitting the same total work into small chunks keeps the page responsive without making it finish sooner.
Method
Laboratory available
The instrument for this investigation runs in the laboratory, where the controls, the live model and the observation log share one workstation.
Enter the laboratoryThis bench runs genuine synchronous work on your main thread and measures the frames that were lost. Compare a single blocking task with the same work split into chunks, and with the same work moved to a worker.
What we observed
| Task length | Perceived as |
|---|---|
| < 16ms | Free, fits in a frame |
| 50ms | A long task by definition; one dropped frame or two |
| 100ms | Noticeable hesitation on click |
| 300ms | Broken. Users click again. |
| 1000ms+ | Users assume the page crashed |
Why it happens
The main thread runs JavaScript, style, layout, paint and most event handling. While a task runs, none of the others can, the page cannot even repaint a pressed button state. Chunking does not reduce total work; it inserts rendering opportunities so the browser can service input and paint between chunks.
// 1. Yield explicitly (Chromium; fall back to a task split elsewhere)for (const chunk of chunks) { process(chunk); if (navigator.scheduling?.isInputPending?.()) await scheduler.yield();}// 2. Split across tasks, works everywhereconst yieldToMain = () => new Promise((r) => setTimeout(r, 0));// 3. Move it off the thread entirelyconst worker = new Worker(new URL("./heavy.worker.js", import.meta.url), { type: "module" });worker.postMessage(payload);Why can't the browser just interrupt my function?
Because JavaScript on the main thread has run-to-completion semantics.
Further research
The Long Animation Frames API (LoAF) reports frames that took too long together with attribution: which script, which source location, how much was style and layout. It is significantly more actionable than the older Long Tasks API, which could tell you that something took 300ms but not what.
References
- 01web.devOptimize long tasks
- 02web.devLong Animation Frames API
- 03MDNUsing Web Workers