DEV Community

Cover image for Deep Dive in Asynchrony: Microtasks, Macrotasks, and the Event Loop
Devesh rajawat
Devesh rajawat

Posted on

Deep Dive in Asynchrony: Microtasks, Macrotasks, and the Event Loop

JavaScript’s asynchronous nature can feel like magic until you dive into the mechanics. The secret sauce lies in its event loop, which orchestrates two key players: microtasks and macrotasks. But what are they, how do they work, and why do they matter? Let’s unravel the mystery with a deep dive, examples, and tips to master this concept.


The Event Loop and Task Queues

The JavaScript engine executes code in a single thread. To handle asynchronous operations, it relies on the event loop, which coordinates between the call stack and task queues. These task queues are split into two categories: microtasks and macrotasks.

Microtasks

Microtasks are high-priority tasks that must be executed as soon as the currently executing JavaScript code finishes and the call stack is empty. They ensure quick follow-up actions and consistent states. Common examples include:

  • Promises (.then, async/await)
  • MutationObserver callbacks

Macrotasks

Macrotasks are lower-priority tasks that the event loop handles only after all microtasks have been executed. They handle larger, deferred operations and external events. Common examples include:

  • Timers (setTimeout, setInterval)
  • MessageChannel callbacks
  • I/O operations

There’s also requestAnimationFrame, which isn’t part of either queue. It synchronizes with the browser’s rendering cycle, making it ideal for smooth animations.


How It Works

Here’s how the event loop processes tasks:

  1. Executes synchronous code first.
  2. Clears the microtask queue.
  3. Executes one task from the macrotask queue.
  4. Repeats steps 2 and 3 until all tasks are handled.

This prioritization ensures that high-priority tasks like promises are resolved before less urgent operations like timers.


An Example in Action

Below is a practical code snippet to illustrate the interaction between synchronous code, microtasks, macrotasks, and requestAnimationFrame:

console.log('Synchronous code starts');

// Macrotask: setTimeout
setTimeout(() => {
  console.log('Macrotask: setTimeout');
}, 0);

// Macrotask: setInterval
const intervalId = setInterval(() => {
  console.log('Macrotask: setInterval');
  clearInterval(intervalId);
}, 100);

// Microtask: Promise
Promise.resolve().then(() => {
  console.log('Microtask: Promise then 1');
  Promise.resolve().then(() => {
    console.log('Microtask: Promise then 2');
  });
});

// Microtask: MutationObserver
const observer = new MutationObserver(() => {
  console.log('Microtask: MutationObserver');
});
const targetNode = document.createElement('div');
observer.observe(targetNode, { attributes: true });
targetNode.setAttribute('data-test', 'true');

// Macrotask: MessageChannel
const channel = new MessageChannel();
channel.port1.onmessage = () => {
  console.log('Macrotask: MessageChannel');
};
channel.port2.postMessage('Test');

// requestAnimationFrame
requestAnimationFrame(() => {
  console.log('Outside task queues: requestAnimationFrame');
});

console.log('Synchronous code ends');
Enter fullscreen mode Exit fullscreen mode

Expected Output

The output sequence helps clarify the prioritization:

  1. Synchronous Code: “Synchronous code starts”, “Synchronous code ends”
  2. Microtasks:
    • “Microtask: Promise then 1”
    • “Microtask: Promise then 2”
    • “Microtask: MutationObserver”
  3. Macrotasks:
    • “Macrotask: setTimeout”
    • “Macrotask: MessageChannel”
    • “Macrotask: setInterval”
  4. requestAnimationFrame: “Outside task queues: requestAnimationFrame”

Deep Dive: Microtasks vs. Macrotasks

Microtasks: Key Characteristics

  • Execute immediately after the synchronous code completes.
  • Ideal for small, high-priority updates like resolving promises or reacting to DOM mutations.
  • Examples: Promises, MutationObserver.

Macrotasks: Key Characteristics

  • Run only after all microtasks are cleared.
  • Used for larger, lower-priority operations or external event handling.
  • Examples: Timers, MessageChannel.

requestAnimationFrame: The Odd One Out

Though not part of the task queues, requestAnimationFrame plays a unique role in asynchrony. It schedules code to run before the next browser repaint, ensuring minimal frame drops and smoother animations.


Conclusion

The interplay between microtasks, macrotasks, and the event loop is at the heart of JavaScript’s asynchrony. By understanding and leveraging these concepts, you can write more efficient, maintainable, and performant code. Remember: microtasks first, macrotasks second, and requestAnimationFrame for visual polish. Happy coding!

Top comments (0)