· tips · 7 min read
Microtasks vs Macrotasks: The Great JavaScript Showdown
A deep dive into microtasks and macrotasks in JavaScript: what they are, how the event loop treats them, how they affect rendering and UX, illustrative diagrams and real-world examples, plus practical guidance to avoid performance pitfalls.

Why this matters
Every web developer eventually runs into a mysterious ordering problem: “Why did my animation not start until after this Promise resolved?” or “Why is my UI frozen despite using Promises?” Understanding microtasks and macrotasks - and how the JavaScript event loop schedules them - is essential for predictable async behavior and snappy user experiences.
This article explains the differences, shows diagrams and real-world scenarios, and offers practical rules to design responsive apps.
Definitions (short)
- Macrotasks (often just called “tasks”): scheduled work placed into a task queue. Examples:
setTimeout
,setInterval
,setImmediate
(Node), I/O callbacks, UI events. - Microtasks: very short work scheduled to run after the currently executing script but before rendering and before the next macrotask. Examples:
Promise.then
/catch
/finally
,queueMicrotask
,MutationObserver
callbacks, andprocess.nextTick
(Node-specific) - thoughprocess.nextTick
has special semantics in Node’s event loop.
References: the MDN event loop explainer and Jake Archibald’s deep dive are excellent reads:
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
- https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
The event loop simplified - lifecycle diagram
A simplified loop for a browser environment:
- Take the next macrotask from the macrotask queue and run it (this includes top-level script execution and e.g. a
setTimeout
callback). - When that macrotask finishes, run all microtasks currently in the microtask queue (and keep running newly enqueued microtasks until the microtask queue is empty).
- Optionally render (repaint/reflow) if needed.
- Repeat.
ASCII diagram:
[ macrotask N ] --exec--> // runs until completion
\__microtasks enqueued____/
run all microtasks (empty queue)
if (needsPaint) -> paint frame
[ macrotask N+1 ] ...
Key point: microtasks run between macrotasks and must drain completely before rendering or processing the next macrotask.
Quick ordering example
Run this code in the browser console and watch the order of the logs:
console.log('script start');
setTimeout(() => console.log('macrotask: setTimeout'), 0);
Promise.resolve().then(() => console.log('microtask: promise1'));
Promise.resolve().then(() => console.log('microtask: promise2'));
console.log('script end');
Expected log order:
- script start
- script end
- microtask: promise1
- microtask: promise2
- macrotask: setTimeout
Why: the top-level script is a macrotask. After it completes, the microtask queue is drained, then the next macrotask (the setTimeout
callback) runs.
Real-world scenario 1 - small microtasks that shuffle control immediately
Imagine you have a small state update that must be applied before the UI paints. Microtasks are perfect: use Promises or queueMicrotask
to ensure the work finishes before the paint.
Example: batching DOM updates done in a click handler. If you schedule DOM writes in a microtask, they are applied before the browser paints, so the user sees the final state in the same frame.
button.addEventListener('click', () => {
// do some synchronous work
// schedule a tiny continuation that must run before paint
Promise.resolve().then(() => {
// final DOM update before paint
});
});
Real-world scenario 2 - microtask starvation (and why you should be careful)
Because the event loop drains microtasks fully before painting, code that continually queues microtasks can starve the rendering and macrotask queues - leading to UI freezes and dropped frames.
A contrived but illustrative example:
function spamMicrotasks() {
Promise.resolve().then(() => {
console.log('tick');
// requeue another microtask indefinitely
spamMicrotasks();
});
}
spamMicrotasks();
setTimeout(() => console.log('macrotask fired'), 0);
In this example, the microtask queue never empties because each microtask enqueues another microtask. The setTimeout
macrotask and the browser paint will not occur - the UI freezes.
Practical takeaway: avoid recursively queuing microtasks without a termination/threshold. For long-running work, chunk it into macrotasks (e.g. setTimeout
, requestIdleCallback
, or a Web Worker).
Real-world scenario 3 - long macrotasks block everything
If a macrotask runs for too long (for example, a heavy computation inside an event handler), it blocks the main thread, preventing microtasks, rendering, user input handling, and timers from running until it finishes.
Example of a blocking macrotask:
button.addEventListener('click', () => {
// heavy synchronous work - blocks UI
const start = performance.now();
while (performance.now() - start < 2000) {
// busy-wait 2 seconds
}
console.log('heavy work done');
});
This blocks the UI for 2 seconds. Fixes include:
- Offload to a Web Worker
- Break work into smaller chunks and schedule via
setTimeout(..., 0)
orrequestIdleCallback
- Use streaming or incremental algorithms
Example: animation timing - setTimeout vs requestAnimationFrame vs microtasks
You want to animate a DOM element at 60 FPS. Where to schedule frame updates?
requestAnimationFrame
(rAF): best for visual updates - the browser calls callbacks right before a paint. Use rAF for smooth animation.setTimeout(..., 16)
approximates 60 FPS, but it’s not aligned with paint and is clamped when inactive.- Microtasks are not a substitute for per-frame updates: they run immediately after JS execution and before paint, but they do not provide a recurring frame tick synchronized to the browser’s refresh.
Example animation loop (correct):
function frame(t) {
// update DOM state for this frame
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
If you instead used microtasks or setTimeout(0)
you will either generate too many synchronous updates (microtasks) or unsynchronized updates (setTimeout) and get janky results.
Node.js differences
Node.js has a different event loop model influenced by libuv. Important differences:
process.nextTick
runs before other microtasks - it is prioritized and can starve the event loop if abused.setImmediate
schedules a macrotask that runs in the check phase.
Official Node docs: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
Be careful with process.nextTick
- use it sparingly for internal bookkeeping.
Diagrams: timeline and queues
A simplified timeline for a single frame (browser):
Time ->
[macrotask: event or script start] -> finish
-> drain microtask queue (all Promise.then / MutationObserver / queueMicrotask)
-> if microtask queue enqueues more microtasks, continue draining
-> paint (if layout/paint needed)
-> next macrotask
Queue view:
Macrotask queue: [taskA, taskB, setTimeout(fn), ...]
Microtask queue: [promiseThen1, mutationObserverCallback, ...]
When a macrotask runs, microtasks accumulate; the loop empties microtasks before rendering/next macrotask.
Practical rules and best practices
- Use microtasks for tiny continuations that must run immediately after current code (e.g., completing Promise-based state transitions). They are perfect for short, deterministically ordered updates.
- Avoid unbounded microtask loops that re-enqueue themselves - they can starve rendering and macrotasks. If you need repeated async work, prefer macrotasks with controlled pacing (
setTimeout
,requestAnimationFrame
,requestIdleCallback
). - For UI animations, prefer
requestAnimationFrame
for per-frame logic. Don’t rely onPromise.then
/microtasks orsetTimeout(0)
for animation timing. - For heavy CPU-bound work, offload to Web Workers or chunk it using macrotasks so the UI can remain responsive.
- In Node, be cautious with
process.nextTick
- it runs before other microtasks and can starve the loop. UsesetImmediate
or Promises for lower priority continuation. - Use
queueMicrotask
if you need to explicitly schedule a microtask without creating a Promise.
Common patterns and fixes
“UI not updating until after Promise” - expected: microtasks run before paint; if you need the browser to paint first, schedule a macrotask (e.g.,
setTimeout
) or userequestAnimationFrame
.“I want something to run after the next paint” - use
requestAnimationFrame
followed by a macrotask if you need something after paint; or use two rAFs (callback runs before paint), or the experimentalwindow.onafterpaint
type patterns (not standardized broadly). Often the pattern is:
requestAnimationFrame(() => {
// runs before paint
requestAnimationFrame(() => {
// runs before paint of the next frame; effectively one paint has occurred in between
});
});
- “Avoid blocking long JS on main thread” - use workers or chunking via
setTimeout(..., 0)
/requestIdleCallback
where appropriate.
Appendix: small, runnable ordering examples
Example: mixing MutationObserver
, Promise.then
and setTimeout
:
console.log('script start');
const el = document.createElement('div');
const mo = new MutationObserver(() =>
console.log('mutation observer callback (microtask)')
);
mo.observe(el, { attributes: true });
Promise.resolve().then(() => console.log('promise then (microtask)'));
setTimeout(() => console.log('timeout (macrotask)'), 0);
el.setAttribute('data-x', '1'); // triggers MutationObserver microtask
console.log('script end');
Likely ordering:
- script start
- script end
- promise then (microtask)
- mutation observer callback (microtask)
- timeout (macrotask)
Note: microtasks enqueued during microtask processing are still processed before the macrotask finishes.
Summary - mental model to keep
- Macrotask = coarse grain scheduling (events, timers). Microtask = fine-grain immediate continuation (Promises, MutationObserver).
- After each macrotask finishes, the engine drains the entire microtask queue before rendering or processing the next macrotask.
- Microtasks can starve rendering if abused. Long macrotasks block everything. Use the correct tool for the job: microtasks for tiny continuations, macrotasks (and workers) for chunked or heavy work, rAF for animations.
Further reading:
- MDN: Event loop - https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
- Jake Archibald: Tasks, microtasks, queues and schedules - https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- Node.js guide: Event loop, timers, and nextTick - https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/