Macrotasks and Microtasks
When a browser loads a standard <script> tag (ignoring attributes like defer), the execution of that script creates the initial macrotask. Within this execution context, various operations occur. For instance, if you set up a click listener or initiate a network request (like fetch), the callback functions for these events are queued to run as separate macrotasks later.
However, if your code utilizes APIs like Promise.then() or MutationObserver, their callback functions behave differently. These are designated as microtasks. They do not wait for the next event loop cycle; instead, they execute immediately after the current macrotask finishes but before the browser proceeds to the rendering phase.
The lifecycle of a running application follows this recurring pattern:
Macrotask (e.g., Script execution)
-> Microtask Queue Flush
-> Rendering
-> Macrotask
-> Microtask Queue Flush
-> Rendering ...
This continuous cycle is known as the Event Loop.
Practical Execution Example
To visualize the priority, consider the following snippet. Note that setTimeout creates a macrotask, while Promise callbacks create microtasks.
// Immediate microtasks
Promise.resolve().then(() => console.log('Micro A'));
Promise.resolve().then(() => console.log('Micro B'));
// Macrotasks scheduled for later
setTimeout(() => {
console.log('Macro X');
Promise.resolve().then(() => console.log('Inner Micro X'));
}, 0);
setTimeout(() => console.log('Macro Y'), 0);
setTimeout(() => console.log('Macro Z'), 0);
// More immediate microtasks
Promise.resolve().then(() => console.log('Micro C'));
Promise.resolve().then(() => {
console.log('Micro D');
Promise.resolve().then(() => console.log('Inner Micro D'));
});
Promise.resolve().then(() => console.log('Micro E'));
The execution logic breaks down as follows:
- Initial Macrotask: The script runs. It schedules three
setTimeoutcallbacks (Macro X, Y, Z) and severalPromisecallbacks (Micro A, B, C, D, E). - Microtask Queue Draining: Before any rendering or next macro occurs, the engine empties the microtask queue. It prints
Micro A,Micro B,Micro C. When it hitsMicro D, that function queuesInner Micro D. Because the microtask queue must be fully cleared before moving on,Inner Micro Dexecutes immediately afterMicro D, followed byMicro E. - Next Macrotask: The Event Loop picks the first
setTimeout(Macro X). It printsMacro Xand queuesInner Micro X. Once the macro finishes, the microtask queue is flushed again, printingInner Micro X. - Subsequent Macrotasks: Finally, Macro Y and Macro Z execute in their respective turns.
A key detail often missed is the behavior of the Promise constructor. If we add the following line to the end of the script:
new Promise((resolve) => console.log('Sync Constructor Logic'));
Despite being at the bottom of the file, Sync Constructor Logic prints first. This is because the function passed to the new Promise executor runs synchronously within the current macrotask, not as a microtask.
Implications for Vue's $nextTick
The browser's event loop order (Macrotask -> Microtask -> Render) explains how Vue.js updates the DOM. Vue batches DOM updates and typically applies them within a macrotask or tick. However, Vue.nextTick (or this.$nextTick) allows you to run code after that DOM update has occurred but before the browser has actually painted the pixels to the screen.
Since microtasks run after the synchronous update logic but before rendering, $nextTick utilizes the microtask queue (or a fallback like setTimeout if microtasks aren't available) to ensure you have access to the updated DOM properties.
Consider this demonstration:
// Assume the background is currently yellow
document.body.style.backgroundColor = 'yellow';
// Update to red (Simulating a Vue reactivity update)
document.body.style.backgroundColor = 'red';
// Check the value inside a microtask
Promise.resolve().then(() => {
let startTime = Date.now();
// Block the thread for 3 seconds
while(Date.now() - startTime < 3000) {
// During this entire 3s block, the console shows 'red'
// but the visual page remains 'yellow' until this thread releases
console.log(document.body.style.backgroundColor);
}
});
This proves that the DOM property is updated to 'red' immediately in the JavaScript context (accessible in the microtask), eventhough the visual rendering happens after the microtask queue is empty.