Mastering JavaScript Event Loop: How Browsers and Node Handle Async Tasks
This article demystifies the JavaScript event loop, explaining how browsers and environments like Node.js manage asynchronous tasks through call stacks, macro‑ and micro‑task queues, and various implementation libraries, illustrated with clear code examples and comparisons across Chrome, Firefox, Safari, Edge, Electron, and Deno.
I recently participated in an interview and discussed event loop details, so I wrote this article to review.
Browser event loop is essential for understanding JavaScript asynchronous programming. It determines how code executes, handles events, and interacts with Web APIs.
Background
Process is the smallest unit of CPU resource allocation; thread is the smallest unit of CPU scheduling. A process consists of one or more threads, which share the same memory space.
In Chrome, opening a tab creates a process that may contain multiple threads such as rendering, JS engine, HTTP request, timer, and event threads. When a request is made, a thread is created and destroyed after completion.
JS engine thread and rendering thread are mutually exclusive because JS can modify the DOM; running JS while the UI thread works can cause unsafe rendering. This single‑threaded nature saves memory and context‑switch overhead but can block the main thread on long tasks, which the event loop and async model address.
JS Main Thread
The JS main thread executes code, calls browser APIs, interacts with the DOM, and creates asynchronous tasks that are placed into the task queue or job queue. When the call stack is empty, the event loop provides a new tick. The components are:
Call Stack: LIFO stack where code runs; when empty, the current tick is finished.
Browser API: Bridge between code and browser internals for tasks like setTimeout, AJAX, DOM methods. Callbacks go to the task queue; Promise then callbacks go to the job queue.
Macro‑task Queue: Holds callbacks from setTimeout, AJAX, etc.
Micro‑task Queue: Holds Promise then callbacks, executed before the next macro‑task.
Event Loop: Monitors the call stack and, when empty, provides the next tick.
Note that the event loop is not a separate thread; it works as part of the JS main thread to schedule tasks.
Event Loop Example
The core mechanism can be simplified into steps:
Check if the call stack is empty.
If empty, check if the task queue has tasks.
If tasks exist, push the first task onto the call stack and execute it.
Repeat.
Simple code demonstration:
<code>console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');</code>Output:
<code>Start
End
Timeout</code>Explanation:
console.log('Start') is pushed onto the call stack and prints Start .
setTimeout registers a timer, pushes its callback to the task queue, and is removed from the stack.
console.log('End') is pushed onto the stack and prints End .
When the stack becomes empty, the event loop moves the timer callback from the task queue to the stack, printing Timeout .
Macro‑tasks include setTimeout, setInterval, I/O operations; micro‑tasks include Promise callbacks and MutationObserver callbacks. Micro‑tasks are processed before any macro‑task in each loop iteration.
Combined macro‑ and micro‑task example:
<code>console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('Script end');</code>Output:
<code>Script start
Script end
Promise
setTimeout</code>Explanation:
console.log('Script start') prints Script start .
setTimeout registers a timer and places its callback in the macro‑task queue.
Promise.resolve().then places its callback in the micro‑task queue.
console.log('Script end') prints Script end .
When the stack is empty, the event loop processes the micro‑task queue first, printing Promise .
Then it processes the macro‑task queue, printing setTimeout .
Event Loop Types
Different browsers and runtimes implement the event loop using various libraries.
Chrome (Chromium)
Event Loop Library: libevent
Description: Uses libevent to manage low‑level async I/O and implements the Message Loop across threads such as the main thread and I/O thread.
Firefox
Event Loop Library: nsIThread and nsIEventTarget
Description: Implements its own loop using these interfaces for task scheduling.
Safari (WebKit)
Event Loop Library: CFRunLoop on macOS/iOS, GLib on other platforms
Description: Uses Core Foundation's CFRunLoop for Apple platforms and GLib elsewhere.
Edge (Chromium‑based)
Event Loop Library: libevent (same as Chrome)
Description: Shares Chromium's event loop implementation after moving to the Chromium engine.
Node.js
Event Loop Library: libuv
Description: libuv provides a cross‑platform async I/O layer that powers Node's event loop.
Electron
Event Loop Library: libevent and libuv
Description: Combines Chromium's libevent with Node's libuv to handle both browser and Node environments.
Deno
Event Loop Library: tokio and rusty_v8
Description: Uses Rust's tokio runtime for async operations and rusty_v8 to integrate the V8 engine.
Summary
The event loop is the core mechanism that enables JavaScript to handle asynchronous operations efficiently. While browsers and runtimes like Node.js, Electron, and Deno implement it with different libraries, they all share the goal of providing high‑performance async I/O and event handling in a single‑threaded environment, allowing developers to write responsive applications.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
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.