On this page
The core idea
JavaScript in the browser runs your code on a single main thread. The event loop takes one task at a time, such as a click handler or a timer callback, and runs it to completion. Only when the call stack is empty does the browser get a chance to render or handle the next event. Promise callbacks are microtasks: they run after the current task finishes but before the browser renders. A long synchronous loop, or a long chain of microtasks, therefore blocks painting and input just as surely as a heavy calculation.
A practical approach
Step 1
Reproduce the freeze with the browser's Performance panel recording. Look for a long task (over 50 milliseconds) and expand it to find which function is running.
Step 2
Identify whether the work is synchronous computation, a large DOM update or a flood of microtasks. Each needs a different fix.
Step 3
For heavy computation, move it off the main thread with a Web Worker, or split it into chunks and yield between them with setTimeout or scheduler.yield where supported.
Step 4
For large DOM updates, build content in a fragment and insert it once, or render only the visible rows of a long list.
Step 5
Give immediate feedback before the expensive work starts: disable the button and show progress, then let the browser render by yielding once before continuing.
Worked example
A catalogue page filters 10,000 course items when the learner types in a search box. Each keystroke runs a synchronous filter, sorts the results and rebuilds the whole list, so typing feels stuck. Recording the interaction shows 300-millisecond long tasks. Debouncing the input, filtering on a precomputed lowercase index and rendering only the first 24 results reduces each task to a few milliseconds, and the input stays responsive.
A mistake to avoid
Do not wrap slow code in a Promise and assume it now runs in the background. new Promise(resolve => { heavyWork(); resolve(); }) still runs heavyWork synchronously on the main thread. Promises organise asynchronous results; they do not create threads.
Try it yourself
Write a button handler that counts to 500 million in a loop and updates a label. Observe that the label never shows intermediate values. Then rewrite it to process the count in chunks with a yield between them, and compare the two in the Performance panel.
Your implementation checklist
- I can find long tasks in a performance recording
- Expensive work is chunked, debounced or moved to a worker
- Large DOM changes are batched or virtualised
- Users get feedback before slow work begins
- I do not expect Promises alone to make code non-blocking
Share or cite this guide
Innovate with Ajay Editorial (2026-10-07). Read the JavaScript Event Loop through One Slow Click Handler. https://innovatewithajay.com/blogs/read-the-javascript-event-loop
