Can block JavaScript execution while running.
Synchronous code blocks the main thread until it finishes — long loops, big JSON.parse, sync XHR, alert/confirm/prompt, document.write, deep recursion. While blocked, no events, no rendering, no timers. Mitigate: break into chunks (yield to microtask/macrotask), use Web Workers for CPU, async APIs only, and avoid the listed blocking calls in production.
JS is single-threaded. Anything synchronous on the main thread halts everything else — events queue up, rendering pauses, timers delay.
What blocks
| Cause | Notes |
|---|---|
| Long synchronous loop | Tight for over a million items, regex backtracking. |
| Big sync work | JSON.parse(huge), JSON.stringify(huge), large structuredClone. |
alert, confirm, prompt | Blocks JS and rendering until the user clicks. |
document.write after parse | Forces a reflow; obsolete. |
| Synchronous XHR | new XMLHttpRequest(); xhr.open(..., false) — blocks until response. Removed/deprecated. |
| Sync localStorage on huge values | localStorage is synchronous API; large reads/writes stutter. |
| Deep recursion | Stack overflow possible. |
while (true) / busy-wait loops | Famous footgun. |
Symptoms
- "Page Unresponsive" dialog.
- Frozen UI; no scroll, no clicks, no animations.
- Time-to-Interactive (TTI) regressions.
- Bad INP scores.
Detection
- Long Tasks API —
PerformanceObserverreports tasks > 50ms.
``js new PerformanceObserver((list) => { for (const t of list.getEntries()) console.warn("long task", t); }).observe({ entryTypes: ["longtask"] }); ``
- DevTools Performance panel shows main-thread task bars.
Mitigations
Yield to event loop
async function chunked(work) {
let i = 0;
while (i < work.length) {
for (let j = 0; j < 1000 && i < work.length; j++) doOne(work[i++]);
await new Promise(r => setTimeout(r)); // or scheduler.yield()
}
}Splits a long task into smaller ones; lets paints and events through.
React 18 transitions
startTransition(() => setLargeState(big)) marks updates as non-urgent; React can interrupt them.
Web Worker
const w = new Worker("parser.js");
w.postMessage(json);
w.onmessage = (e) => use(e.data);Heavy compute (parsing, image processing, search indexing) lives on its own thread.
Streaming JSON parse
For big payloads, stream + parse line by line (ndjson) instead of single JSON.parse. Or use a streaming parser library.
Avoid blocking APIs
- Never
alert/confirm/promptin production — use a custom modal. - Never sync XHR. Fetch is async.
- Move heavy localStorage / IndexedDB work off the main thread.
Long sync work in React
useMemofor expensive derivations.useDeferredValuefor non-urgent renders.startTransitionfor navigation / list updates.- Workers for CPU-heavy compute (e.g. PDF render, regex compile).
Interview framing
"JS is single-threaded; anything synchronous on the main thread blocks events, rendering, and timers. Common culprits: long loops, big JSON.parse, alert/confirm, sync XHR (deprecated), localStorage with large values, document.write. Detect with the Long Tasks API or DevTools. Mitigations: break long work into chunks with await + setTimeout, use React 18 transitions to defer non-urgent updates, move CPU-heavy work to a Web Worker. Avoid alert/confirm and sync XHR in production. The INP metric rewards apps that keep tasks under 50ms."
Follow-up questions
- •What is the Long Tasks API?
- •When would you use a Web Worker?
- •How does startTransition help?
Common mistakes
- •JSON.parse on a 50MB string.
- •alert/confirm in modern UIs.
- •Tight loops without yielding.
Performance considerations
- •Anything > 50ms hurts INP. Worker offload + chunking + transitions are the levers.
Edge cases
- •Microtask infinite loop (also blocks main thread).
- •Sync XHR still works in some legacy code.
- •Workers don't have DOM access.
Real-world examples
- •PDF.js workers, image processing in workers, big-list virtualization, INP optimizations.