JavaScript tutorials > Advanced Concepts > Asynchronous JavaScript > What are microtasks and macrotasks in JavaScript?

What are microtasks and macrotasks in JavaScript?

Understanding microtasks and macrotasks is crucial for writing efficient and predictable asynchronous JavaScript code. They are the building blocks of the event loop, which manages the execution of tasks in a non-blocking manner. This tutorial will explain the concepts with clear examples.

The JavaScript Event Loop

JavaScript is single-threaded, meaning it can only execute one operation at a time. The event loop is what allows JavaScript to perform non-blocking operations, like handling user events, fetching data from a server, or setting timers, without freezing the user interface.

The event loop continuously monitors the call stack and the task queues (macrotask and microtask queues). If the call stack is empty, it picks a task from one of the queues and pushes it onto the call stack for execution.

Macrotasks (Task Queue)

Macrotasks represent larger, discrete units of work. They are added to the macrotask queue. Examples of macrotasks include:

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • I/O operations (e.g., network requests, file system access)
  • Rendering updates
  • User interaction events (click, keypress)

The event loop processes one macrotask at a time, from the oldest to the newest. After processing a macrotask, the event loop will then check the microtask queue.

Microtasks (Microtask Queue)

Microtasks are smaller, more immediate tasks that need to be executed as soon as possible after the current macrotask completes and before the browser has a chance to re-render or handle other events. Microtasks are added to the microtask queue. Examples of microtasks include:

  • Promises (.then(), .catch(), .finally())
  • queueMicrotask()
  • Mutation Observer callbacks
  • process.nextTick() (Node.js)

Crucially, the microtask queue is processed completely after each macrotask. This means all pending microtasks will be executed before the event loop moves on to the next macrotask. If new microtasks are added while the queue is being processed, they will also be executed before the next macrotask.

Code Example: Macrotasks vs. Microtasks

Here's a code example that demonstrates the difference between macrotasks and microtasks:

Output:

Script start
Script end
queueMicrotask
Promise.then
setTimeout

Explanation:

  1. The script starts and logs 'Script start'.
  2. A setTimeout callback is scheduled (macrotask) and added to the macrotask queue.
  3. A Promise is resolved, and its .then() callback is scheduled (microtask) and added to the microtask queue.
  4. A queueMicrotask() callback is added to the microtask queue.
  5. The script logs 'Script end'.
  6. The call stack is now empty. The event loop first processes the microtask queue.
  7. queueMicrotask is executed, logging 'queueMicrotask'.
  8. Promise.then is executed, logging 'Promise.then'.
  9. The microtask queue is now empty. The event loop picks the next macrotask, which is the setTimeout callback.
  10. The setTimeout callback is executed, logging 'setTimeout'.

console.log('Script start');

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

Promise.resolve().then(() => {
  console.log('Promise.then');
});

queueMicrotask(() => {
    console.log('queueMicrotask');
});

console.log('Script end');

Concepts Behind the Snippet

The code demonstrates the priority given to microtasks over macrotasks. Even though the setTimeout has a delay of 0 milliseconds, the microtasks from the Promise and queueMicrotask are executed first. This is because microtasks are processed immediately after the current macrotask finishes and before the event loop moves on to the next macrotask.

Real-Life Use Case: UI Updates

Imagine you have a button that, when clicked, needs to update the UI with some data fetched from an API. You might use Promises to handle the asynchronous API call. The .then() callbacks, which handle the UI updates, will be executed as microtasks. This ensures that the UI is updated immediately after the data is received and before the browser repaints, providing a smoother user experience.

Best Practices

  • Avoid long-running microtasks: If your microtasks take too long to execute, they can block the event loop and cause performance issues.
  • Use microtasks for essential, immediate tasks: Microtasks are ideal for tasks that must be executed as soon as possible after the current macrotask.
  • Be mindful of the execution order: Understand that microtasks will always be executed before the next macrotask.
  • Use queueMicrotask for scheduling microtasks: It is the standard API now, better than relying on promise implementation details.

Interview Tip

During interviews, be prepared to explain the event loop, the difference between macrotasks and microtasks, and provide examples of each. You should also be able to trace the execution order of asynchronous code snippets.

When to Use Them

  • Use Macrotasks: For tasks that can be deferred without impacting the immediate responsiveness of the application (e.g., periodic tasks, I/O operations).
  • Use Microtasks: For tasks that need to be completed before the browser repaints or handles other events (e.g., UI updates after a Promise resolves, ensuring data consistency).

Memory Footprint

Microtasks generally have a smaller memory footprint compared to macrotasks because they are designed to be short-lived and executed quickly. Macrotasks, especially those involving I/O operations or timers, might hold onto resources longer, contributing to a larger memory footprint until they are completed.

Alternatives

While microtasks and macrotasks are fundamental to asynchronous JavaScript, alternative patterns and libraries can help manage asynchronous operations more effectively:

  • Async/Await: Provides a cleaner syntax for working with Promises, making asynchronous code look and behave more like synchronous code. It doesn't replace microtasks/macrotasks but makes them easier to manage.
  • RxJS (Reactive Extensions for JavaScript): A library for reactive programming that uses observables to manage asynchronous data streams and events. It provides powerful operators for transforming, filtering, and combining asynchronous data.
  • Web Workers: Allow you to run JavaScript code in the background, separate from the main thread, preventing UI blocking. This is suitable for computationally intensive tasks that would otherwise freeze the UI.

Pros

Microtasks:

  • Ensure timely execution of critical tasks.
  • Provide a way to guarantee data consistency and immediate UI updates.

Macrotasks:

  • Enable non-blocking operations.
  • Allow for deferred execution of tasks.

Cons

Microtasks:

  • Long-running microtasks can block the event loop.
  • Can lead to unexpected behavior if not carefully managed.

Macrotasks:

  • Tasks may experience delays due to queueing.

FAQ

  • What happens if a microtask adds another microtask?

    If a microtask adds another microtask to the queue, the new microtask will also be executed during the same microtask processing phase, before the event loop moves on to the next macrotask. This continues until the microtask queue is empty.
  • Can I use microtasks to replace macrotasks?

    No. Microtasks are designed for short, immediate tasks that must be executed before the browser has a chance to repaint or handle other events. Macrotasks are for larger, more discrete units of work that can be deferred without impacting the immediate responsiveness of the application. Using microtasks to replace macrotasks can lead to performance issues.
  • How do web workers relate to macrotasks and microtasks?

    Web Workers run in a completely separate thread from the main thread, allowing them to perform long-running tasks without blocking the UI. While web workers have their own event loop, they communicate with the main thread via message passing. The receipt and processing of messages sent from a web worker to the main thread occurs as a macrotask.