Why Your UI Doesn't Freeze: Inside JS Event Loop and Microtask Queues

Introduction

JavaScript powers interactive web apps through its single-threaded model paired with clever async handling. This post breaks down the event loop and microtasks, explaining how they prevent blocking while keeping code predictable.

Core Components

  • Call Stack: Executes JS synchronously, one frame at a time (LIFO).

  • Web APIs: Browser handles timers, fetch, DOM outside the JS thread.

  • Task Queue (Macrotasks): Holds callbacks from setTimeout, events, I/O.

  • Microtask Queue: Prioritizes Promises, queueMicrotask, MutationObserver.

  • Event Loop: Monitors stack; when empty, processes microtasks first, then macrotasks.

How the Event Loop Works

  1. JS runs synchronously on the call stack.

  2. Async ops delegate to Web APIs.

  3. Completed callbacks queue: Promises → microtask queue; others → task queue.

  4. Loop checks: drain all microtasks, then one macrotask, repeat.

Microtasks vs Macrotasks

AspectMicrotasksMacrotasks
PriorityHigher (always before next macrotask)Lower
ExamplesPromise.then(), queueMicrotask()setTimeout, UI events
Use CaseChain async without yielding controlSchedule non-urgent work

Common Pitfalls

  • Starvation: Infinite microtasks block macrotasks (e.g., recursive Promises).

  • Order Matters: console.log in macrotask sees microtask results.

  • Example:

js
setTimeout(() => console.log('Macro')); Promise.resolve().then(() => console.log('Micro'));

Output: Micro → Macro.

Visual Flow

text
JS Call Stack (empty?) → Microtasks → Macrotasks → Repeat

Practical Tips

  • Use microtasks for immediate async chaining.

  • Avoid long-running microtasks to prevent UI freezes.

  • Debug with performance.now() to trace timing.

Conclusion

Mastering the event loop unlocks non-blocking JS. Experiment in browser console to see queues in action.

Comments