In a browser page, a JavaScript click handler runs a four-second synchronous loop (or a JSON.parse of a very large string). Using the call stack, explain why the whole page stops responding until it finishes, and what happens to the clicks the user makes in the meantime.
answer
- one job, one stack, no preemption
- queued work waits for an empty stack
- painting needs the same thread
- input is queued, then delivered in a burst
- JSON.parse cannot be subdivided
basics
~20 sThe handler holds the only call stack for four seconds, and no queued job can start until that stack is empty, so no other handler, timer or promise reaction runs and nothing repaints. Clicks made meanwhile are queued and their handlers fire in a burst afterwards.
solid answer
~50 sThe handler is one job, and a job runs until its call stack is empty. For those four seconds that stack is occupied, so the runtime cannot pick up anything else: no other event handler, no timer callback, no promise reaction, and no repaint — the thread that would do all of that is the one my loop is holding. Meanwhile the host keeps accepting input and queuing work, so the clicks are not lost; their handlers simply cannot start. When my loop finally returns and the stack drains, the backlog runs job after job, which the user experiences as the page catching up all at once. `JSON.parse` is the sharpest version of this because it is a single synchronous call with no incremental form, so I cannot even split it. The only real fixes are much shorter jobs, or doing the work somewhere other than this thread.
code
javascript · 15 linesfunction blockFor(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {}
}
const t0 = Date.now();
setTimeout(() => console.log('timer, asked for 0 ms, ran at', Date.now() - t0), 0);
Promise.resolve().then(() => console.log('promise reaction at', Date.now() - t0));
blockFor(2000);
console.log('sync job done at', Date.now() - t0);
// sync job done at ~2000
// promise reaction at ~2000
// timer, asked for 0 ms, ran at ~2000go deeper
Know that long synchronous work freezes the page because your code holds the single thread, and that no callback can run until that function returns.
Explain it mechanically: the handler is one job, jobs run to completion, so every queued callback and every repaint waits behind it and then fires in a burst when the stack drains.
Bring the production reading: recognise the single unbroken stack in a profile, distinguish a CPU-blocked page from a network-slow one, and call out the duplicate-submit hazard the queued input burst creates.
Own the systemic version: treat a per-job work budget as an enforceable contract, decide which classes of work are simply not permitted on the main thread, and push back on payload sizes that make an indivisible parse unavoidable.
## One stack, one job, no preemption A click handler is a job. The rule for jobs is run-to-completion: once one starts, it runs until the call stack is empty, and the runtime only then takes the next unit of work. Your four-second loop is a frame sitting on the stack that whole time. Nothing about it being slow makes it interruptible — there is no preemption point between iterations, and no mechanism by which the runtime can set your loop aside and come back to it. That single fact explains the entire symptom. Every other thing you might hope happens during those four seconds is itself a job needing the same stack: another element's click handler, a timer callback whose delay expired two seconds ago, a promise reaction for a response that already arrived, a scroll handler. They are all queued and all blocked behind you. ```js function blockFor(ms) { const end = Date.now() + ms; while (Date.now() < end) {} } setTimeout(() => console.log('timer scheduled for 0 ms'), 0); blockFor(2000); console.log('sync work done'); // 'sync work done', then roughly two seconds late: 'timer scheduled for 0 ms' ``` ## Why the pixels stop too Updating what the user sees is not something that happens on a separate track. The page's visual updates are driven from the same thread your script is holding, so while the stack is occupied nothing new is painted. That is why the symptom is total rather than partial: a spinner stops spinning, animations freeze, selection does not respond, and any DOM changes you made *before* the loop started are not visible either — you modified the tree, but nothing has had a chance to reflect it. Users read the whole thing as "the page is broken", and if it lasts long enough the browser may offer to terminate the page. ## Where the clicks go Input is not dropped on the floor. The host is not the JavaScript engine; it keeps receiving operating-system input while your job runs and keeps queuing the corresponding work. What it cannot do is *dispatch* it, because dispatching means running a handler, which means needing an empty stack. When your loop returns and the stack drains, the backlog is processed as a series of jobs in quick succession. The user-visible result is a burst: three impatient clicks on the same button run its handler three times, back to back, at the end. That is a correctness hazard, not just a cosmetic one — anything non-idempotent (submit, purchase, increment) can now happen several times because the user got no feedback that the first press registered. Disabling the control as the very first statement of the handler, before the expensive work starts, removes the multiplier. Note also that input types are not all treated identically; some continuous streams such as pointer moves are coalesced rather than delivered one for one, so do not promise that every event replays. ## Why JSON.parse is the sharpest example A loop you wrote can in principle be restructured: you control the iteration, so you control where the work could be divided. `JSON.parse` gives you no such control. It is one synchronous call that returns only when the whole document has been parsed and the whole object graph built; there is no callback, no chunked mode, no partial result. From the stack's point of view it is a single frame that will not pop for as long as it takes. The same is true of `JSON.stringify` over a large graph. When the expensive thing is one built-in call, subdividing it is not on the table at all, and the remaining moves are to shrink the payload, split it into several documents parsed in separate jobs, or get the work off this thread entirely. ## Recognising it The signature is distinctive precisely because of run-to-completion: a CPU profile shows one continuous span attributable to a single stack, with no gaps, rather than many small entries. Blame is unusually easy to assign — because nothing can interleave, whatever is on that stack is entirely responsible for the stall. A related tell is that timers scheduled during the stall all fire late, clustered at the moment the stack finally empties. Contrast that with a page waiting on a slow network response, where the runtime is idle, other handlers still run, and the page still repaints. ## What can actually be done There are only two structural answers, and both follow directly from the mechanism. Either each job becomes small enough that the runtime regains an empty stack frequently, so queued handlers and painting get their turn in between; or the expensive work stops running on this thread at all and this thread only receives the result. Everything else — moving the call later, wrapping it in a promise, making the surrounding function `async` — changes *when* the job runs, not how long it holds the stack, and therefore does not help. Wrapping a four-second parse in `Promise.resolve().then(...)` still freezes the page for four seconds; it just freezes it slightly later.
- Does wrapping the expensive call in a promise or an async function fix the freeze?No. Promises and `async` change when a job starts, not how long it holds the stack. A four-second parse inside `.then()` is still four seconds of one uninterrupted job — you moved the freeze slightly later rather than removing it. Only reducing the work done per job, or performing it off this thread, changes the outcome.
- How do you tell this apart from a slow network call when the page feels stuck?A blocking job pins the CPU and stalls everything at once — timers fire late, animations stop, and a profile shows one continuous stack with no gaps. A slow network call leaves the runtime idle and responsive: other handlers still run, the page still repaints, and only the region waiting on that data is unpopulated. The CPU signatures are opposites.
- Why is the click burst afterwards a correctness problem rather than a cosmetic one?Because the user got no feedback that the first press registered, so they pressed again. All of those handler invocations then run back to back, and any non-idempotent action — submitting, charging, incrementing — happens once per press. Disabling the control as the first statement of the handler, before the expensive work begins, removes the multiplier.
- Can you shorten a single large JSON.parse the way you can shorten a loop?Not directly. `JSON.parse` is one synchronous call with no incremental or callback form, so from the stack's perspective it is an indivisible frame. The realistic options are to stop sending a payload that large, to split it into several smaller documents parsed in separate jobs, or to perform the parse somewhere other than this thread and receive the result.
saying these in an interview costs you the question
- The browser preempts long-running scripts automatically
- Clicks during the freeze are silently discarded
- Making the handler async stops it blocking
- JSON.parse runs on a background thread because it is a built-in
- Only DOM work blocks the page, pure computation does not