Mastering JavaScript Macrotasks, Microtasks, and Promise Timing
This article explains the differences between macrotasks and microtasks in the JavaScript event loop, shows how they affect Promise execution, provides practical code examples, and discusses how frameworks like Vue implement task scheduling, helping developers avoid dead‑loops and improve performance.
Google Developer Day China 2018 by Jake Archibald
Although I did not attend the event, I read a detailed summary of the browser event loop presented by Baidu's 小蘑菇小哥 and added my own notes.
Below is a concise classification of asynchronous tasks:
Tasks (Macrotasks) – executed one at a time during the current event loop; new tasks created during the loop are queued for later execution. Common sources:
setTimeout,
setInterval,
setImmediate, I/O.
Animation callbacks – run before the render steps (Structure‑Layout‑Paint). All current‑loop tasks are executed first; any new tasks created run in the next loop. Source:
requestAnimationFrame.
Microtasks – executed immediately after the current script finishes, before the next macrotask. The queue is drained completely before the loop ends, and any microtasks created while processing are added to the same queue. Sources:
Promise,
Object.observe,
MutationObserver,
process.nextTick.
Intuitive Demo of Macrotasks vs. Microtasks
The distinction between “mtask” and “task” is debated; some argue there is only one task queue.
For developers new to JavaScript async patterns, the following recursive example illustrates a dead‑loop caused by blocking the event loop:
<ol><li><code>// ordinary recursion causing a dead‑loop, page becomes unresponsive</code></li><li><code>function callback() {</code></li><li><code> console.log('callback');</code></li><li><code> callback();</code></li><li><code>}</code></li><li><code>callback();</code></li></ol>Wrapping the recursive call in
setTimeoutmoves it to the macrotask queue, preventing the UI from freezing:
<ol><li><code>// Macrotask version – no dead‑loop</code></li><li><code>function callback() {</code></li><li><code> console.log('callback');</code></li><li><code> setTimeout(callback, 0);</code></li><li><code>}</code></li><li><code>callback();</code></li></ol>When the same logic is placed inside a
Promise(a microtask), the dead‑loop reappears because microtasks are executed immediately:
<ol><li><code>// Microtask version – still causes a dead‑loop</code></li><li><code>function callback() {</code></li><li><code> console.log('callback');</code></li><li><code> Promise.resolve().then(callback);</code></li><li><code>}</code></li><li><code>callback();</code></li></ol>Key points:
Microtasks run at the end of the current event loop before it finishes.
The loop does not end until the microtask queue is empty.
If a microtask creates another microtask, the loop can never exit, leading to a dead‑loop.
Microtasks and Promise A+
Promises execute their executor function immediately (synchronously) and schedule
thencallbacks as microtasks. The following example demonstrates that the promise body runs right away, while the
thenhandler runs after the current script:
<ol><li><code>var d = new Date();</code></li><li><code>// Create a promise that resolves after 2 seconds</code></li><li><code>var promise1 = new Promise(function(resolve, reject) {</code></li><li><code> setTimeout(resolve, 2000, 'resolve from promise 1');</code></li><li><code>});</code></li><li><code>// Create a promise that resolves after 1 second and resolves promise1</code></li><li><code>var promise2 = new Promise(function(resolve, reject) {</code></li><li><code> setTimeout(resolve, 1000, promise1); // resolve(promise1)</code></li><li><code>});</code></li><li><code>promise2.then(result => console.log('result:', result, new Date() - d));</code></li></ol>Output shows that the executor runs immediately, while the
thencallback is deferred.
Two conclusions:
It validates the Promise/A+ 2.3.2 requirement that
thencallbacks are asynchronous.
Promise constructors execute synchronously (the 2‑second timer starts right away).
The spec also notes that platform code may implement the asynchronous step using either a macrotask (
setTimeout,
setImmediate) or a microtask (
MutationObserver,
process.nextTick).
Why Microtasks Matter
Microtasks are ideal for operations that must happen immediately after the current script, such as batching updates or performing lightweight async work without the overhead of a full macrotask.
Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.
The event loop processes tasks in this order:
Take one macrotask from the queue and execute it.
Drain the entire microtask queue, executing each microtask in order.
When both queues are empty, the loop ends and a new macrotask is taken.
This explains why
Promise.thenis implemented as a microtask: the callback runs as soon as possible after the current script, without waiting for the next macrotask.
Practical Application in Vue
Vue’s
src/core/utils/next-tick.jsimplements both macrotask and microtask strategies to schedule callbacks. The code chooses the fastest available mechanism (e.g.,
setImmediate,
MessageChannel, or
setTimeout) and falls back gracefully when necessary.
<ol><li><code>let microTimerFunc;</code></li><li><code>let macroTimerFunc;</code></li><li><code>if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {</code></li><li><code> macroTimerFunc = () => { setImmediate(flushCallbacks); };</code></li><li><code>} else if (typeof MessageChannel !== 'undefined' && isNative(MessageChannel)) {</code></li><li><code> const channel = new MessageChannel();</code></li><li><code> const port = channel.port2;</code></li><li><code> channel.port1.onmessage = flushCallbacks;</code></li><li><code> macroTimerFunc = () => { port.postMessage(1); };</code></li><li><code>} else {</code></li><li><code> macroTimerFunc = () => { setTimeout(flushCallbacks, 0); };</code></li><li><code>}</code></li><li><code>if (typeof Promise !== 'undefined' && isNative(Promise)) {</code></li><li><code> const p = Promise.resolve();</code></li><li><code> microTimerFunc = () => { p.then(flushCallbacks); };</code></li><li><code>}</code></li></ol>This implementation ensures that Vue’s reactivity system updates efficiently across different browsers and environments.
Note: Some older browsers implement Promise callbacks as macrotasks, which can affect timing and performance.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.