Macrotasks (often just called "tasks") are the lower-priority queue behind setTimeout, setInterval, and browser-driven events like clicks and I/O — understanding how the event loop processes exactly one per tick is key to writing responsive, non-blocking code.
1Macrotasks | JavaScript Tutorial - In-Depth Guide Part 1
A macrotask is a unit of work the event loop processes one at a time — setTimeout/setInterval callbacks, I/O completion, and UI events like clicks all become macrotasks.
setTimeout(() => console.log('macrotask 1'), 0);
setTimeout(() => console.log('macrotask 2'), 0);
// Each runs in a SEPARATE event loop tick, not back-to-backThe Lower-Priority Queue
2Macrotasks | JavaScript Tutorial - In-Depth Guide Part 2
Because only one macrotask runs per tick (with rendering and microtask draining happening in between), the browser gets regular opportunities to repaint and respond to input between macrotasks — which is exactly what keeps a page responsive.
function processInChunks(items, chunkSize) {
function processNext() {
const chunk = items.splice(0, chunkSize);
chunk.forEach(processItem);
if (items.length > 0) setTimeout(processNext, 0); // yield between chunks
}
processNext();
}Yielding Between Chunks
3Macrotasks | JavaScript Tutorial - In-Depth Guide Part 3
setTimeout(fn, 0) doesn't guarantee the callback runs after exactly 0ms — browsers commonly clamp minimum delays (historically 4ms for nested timeouts), and the callback still waits its turn as a macrotask behind any pending microtasks.
setTimeout(() => console.log('runs "immediately", but not really 0ms'), 0);'0ms' Isn't Exact
4Macrotasks | JavaScript Tutorial - In-Depth Guide Part 4
User interactions (clicks, key presses, scroll) are also processed as macrotasks — this is why heavy synchronous work between two macrotasks can make a page feel unresponsive to clicks during that window.
button.addEventListener('click', () => {
heavySynchronousComputation(); // blocks all other macrotasks, including new clicks
});UI Events Are Macrotasks Too
5Macrotasks | JavaScript Tutorial - In-Depth Guide Part 5
Deliberately splitting long-running work into multiple macrotasks (via setTimeout, or the newer scheduler.postTask API) is a direct, practical technique for keeping a page responsive during heavy processing.
async function processLargeArray(items) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
if (i % 100 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0)); // yield
}
}
}Yielding for Responsiveness
6Step-by-Step Breakdown
A macrotask is a unit of work the event loop processes one at a time — setTimeout/setInterval callbacks, I/O completion, and UI events like clicks all become macrotasks.
Checkpoint: Does the event loop process every pending macrotask back-to-back before doing anything else, the way it does with microtasks?
- →Yes, all macrotasks drain fully like microtasks do
- →No, it processes exactly one macrotask per tick
Because only one macrotask runs per tick (with rendering and microtask draining happening in between), the browser gets regular opportunities to repaint and respond to input between macrotasks — which is exactly what keeps a page responsive.
setTimeout(fn, 0) doesn't guarantee the callback runs after exactly 0ms — browsers commonly clamp minimum delays (historically 4ms for nested timeouts), and the callback still waits its turn as a macrotask behind any pending microtasks.
User interactions (clicks, key presses, scroll) are also processed as macrotasks — this is why heavy synchronous work between two macrotasks can make a page feel unresponsive to clicks during that window.
Checkpoint: If a click handler runs 500ms of heavy synchronous computation, can the browser process another click during that time?
- →Yes, clicks are always processed immediately regardless
- →No, the main thread is blocked until that macrotask finishes
Deliberately splitting long-running work into multiple macrotasks (via setTimeout, or the newer scheduler.postTask API) is a direct, practical technique for keeping a page responsive during heavy processing.
Next, we'll explore 'Memory Leaks'.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Yielding to the Main Thread Keeps Keyboard Navigation Responsive During Heavy Work
Breaking long-running work into multiple macrotasks ensures keyboard-driven navigation and focus changes (critical for many assistive technology users) remain responsive throughout, rather than freezing entirely until a large synchronous operation completes.
SEO Implications
- 1
Chunking Long Tasks Directly Improves Interaction to Next Paint
Google's Core Web Vitals specifically penalize "long tasks" that block the main thread for extended periods; splitting heavy work into multiple macrotasks is a direct, practical technique for improving this metric and the associated search ranking signal.
Best Practices
Break Up Long Synchronous Work into Multiple Macrotasks
Chunking heavy computation with setTimeout (or similar yielding techniques) between chunks keeps the page responsive to user input and rendering during long operations.
Don't Rely on setTimeout(fn, 0) for Precise Sub-Millisecond Timing
Browsers clamp minimum timer delays and macrotask processing is subject to queueing behind other work; use requestAnimationFrame or dedicated timing APIs when precision actually matters.
Frequent Bugs
A synchronous loop processing thousands of items in one macrotask, causing the page to become completely unresponsive to clicks and scrolling until it finishes.
Break the loop into chunks, yielding control back to the browser (via setTimeout or a similar mechanism) between chunks so other macrotasks, including UI events, get a chance to run.
Assuming `setTimeout(fn, 0)` executes in exactly 0 milliseconds, and building timing-sensitive logic that depends on that assumption.
Treat 0ms as "as soon as possible, but not guaranteed exact", and use a more precise timing mechanism (like requestAnimationFrame or performance.now()-based checks) if exact timing genuinely matters.
Real-World Examples
Processing a Large Dataset Without Freezing the UI
A data import feature needed to process tens of thousands of records client-side without making the page unresponsive during the operation.
async function importRecords(records) {
const CHUNK_SIZE = 200;
for (let i = 0; i < records.length; i += CHUNK_SIZE) {
const chunk = records.slice(i, i + CHUNK_SIZE);
chunk.forEach(processRecord);
updateProgressBar(i / records.length);
await new Promise((resolve) => setTimeout(resolve, 0)); // yield to the browser
}
}