JavaScript on a page runs on one main thread, which also handles clicks, typing, scrolling and drawing. If your code runs for a long time without a break, nothing else can happen, and the page feels frozen. Anything over about 50 milliseconds is called a long task, and users start to notice.
What a blocked page looks like
button.addEventListener("click", () => {
for (let i = 0; i < 5_000_000_000; i++) { /* heavy loop */ }
statusText.textContent = "Done"; // nothing updates until the loop finishes
});
While the loop runs, the button does not respond and animations stop. (Do not run code like this in the editor.)
Fix 1: do less
Is the work needed at all? Can the server do it? Can you cache or paginate? Removing work always beats scheduling it better.
Fix 2: split the work into chunks
Process a bit, give the browser a chance to breathe, then continue.
const items = Array.from({ length: 50 }, (_, i) => i);
const results = [];
function processChunk(start) {
const end = Math.min(start + 10, items.length);
for (let i = start; i < end; i++) {
results.push(items[i] * 2); // the real work for each item
}
if (end < items.length) {
setTimeout(() => processChunk(end), 0); // yield: let the browser handle other things
} else {
console.log("done", results.length); // → done 50
}
}
processChunk(0);
Between chunks, clicks and painting can happen. For visual updates, requestAnimationFrame is better. Newer browsers also offer scheduler.yield() (check support) to yield in a more precise way.
async/await version
const nextTick = () => new Promise(resolve => setTimeout(resolve, 0));
async function processAll(items) {
let processed = 0;
for (const item of items) {
// ... heavy work for one item ...
processed++;
if (processed % 10 === 0) await nextTick(); // yield every 10 items
}
return processed;
}
processAll(Array.from({ length: 35 })).then(count => console.log("processed", count)); // → processed 35
Fix 3: use idle time
requestIdleCallback runs low-priority work when the browser has nothing else to do.
if ("requestIdleCallback" in window) {
requestIdleCallback(deadline => {
while (deadline.timeRemaining() > 0 && queue.length) {
doSmallTask(queue.shift());
}
});
}
Support is not universal, so use a fallback (such as setTimeout).
Fix 4: use a Web Worker
For genuinely heavy computation, move it to another thread: Web Workers.
Fix 5: let CSS animate
Animations done with CSS (transform, opacity) run on the compositor and stay smooth even when JavaScript is busy.
Make long waits visible
If something must take time, show progress and keep the interface responsive (Loading States).
Finding long tasks
- The browser’s Performance panel marks long tasks in red.
PerformanceObservercan report them from code:
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.log("Long task:", Math.round(entry.duration), "ms");
}
}).observe({ entryTypes: ["longtask"] });
(Support varies. Check compatibility.)