Understanding the JavaScript Event Loop in Browsers and Node.js
This article explains the fundamentals of JavaScript's event loop, covering single‑threaded execution, the role of macro‑tasks and micro‑tasks, differences between browser and Node.js implementations, and includes code examples that illustrate task scheduling and execution order.
Hello, I am CUGGZ. The event loop is a crucial concept in JavaScript, and this article explores its principles in browsers and Node.js, highlighting their differences.
1. Asynchronous Execution Principle
(1) Single‑threaded JavaScript
JavaScript is a single‑threaded language used for user interaction and DOM manipulation. It supports both synchronous and asynchronous operations, which helps avoid code blocking.
Synchronous: The caller receives the expected result immediately when the function returns.
Asynchronous: The caller must obtain the result later through a callback or promise.
Benefits of a single thread:
Prevents UI rendering conflicts because JavaScript can modify the DOM, and concurrent UI work could cause unsafe rendering.
Saves memory and reduces context‑switch overhead.
(2) Multi‑threaded Browser
Although JavaScript runs on a single thread, browsers use multiple threads to handle asynchronous tasks. When JavaScript initiates an async operation, the browser spawns a separate thread (e.g., timer thread, HTTP request thread) to perform the work, while the main JavaScript thread continues execution. After the async work finishes, its callback is queued back to the JavaScript engine thread.
The Chrome architecture includes GUI rendering, JavaScript engine, event trigger, timer, and async HTTP request threads, providing the foundation for asynchronous execution.
2. Browser Event Loop
JavaScript tasks are classified as synchronous or asynchronous . Synchronous tasks run on the main thread one after another; asynchronous tasks are placed in a task queue and later moved to the call stack for execution.
(1) Call Stack and Task Queues
Call Stack: A LIFO stack that stores function calls. Each function creates an execution context; when it finishes, the context is popped.
Task Queue: A FIFO queue that holds asynchronous callbacks. After the call stack is empty, the event loop dequeues tasks and pushes them onto the stack.
The execution order of tasks follows the diagram shown in the original article.
(2) Macro‑tasks and Micro‑tasks
Task queues are divided into macro‑task and micro‑task queues. Common macro‑tasks include script , setTimeout , setInterval , I/O, UI events, and setImmediate (Node). Micro‑tasks include Promise , MutationObserver , and process.nextTick (Node).
The processing order is: take the first macro‑task, execute it, then drain all micro‑tasks generated during that macro‑task before moving to the next macro‑task.
console.log('同步代码1');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('同步代码2');
resolve();
}).then(() => {
console.log('promise.then');
});
console.log('同步代码3');Output:
"同步代码1"
"同步代码2"
"同步代码3"
"promise.then"
"setTimeout"The step‑by‑step explanation shows how synchronous code runs first, macro‑tasks are queued, micro‑tasks are processed before the next macro‑task, and finally the timer callback executes.
3. Node.js Event Loop
(1) Concept
Node.js initializes its own event loop when it starts, processes the input script, and then begins the loop. Besides the browser’s async mechanisms, Node provides additional ones such as file I/O, setImmediate , process.nextTick , and server close callbacks.
(2) libuv Phases
Node’s event loop, implemented by libuv, consists of six phases that run repeatedly: timers , I/O callbacks , idle, prepare , poll , check , and close callbacks . Each phase processes its queue, then drains the micro‑task queue before moving on.
The poll phase handles I/O callbacks and timers; if the queue is empty, the loop may wait or proceed to check for setImmediate callbacks.
(3) Macro‑tasks and Micro‑tasks in Node
Node’s macro‑tasks include setTimeout , setInterval , setImmediate , scripts, and I/O operations. Micro‑tasks include process.nextTick and Promise.then .
(4) process.nextTick()
process.nextTick creates a special queue that runs immediately after the current operation, before the next phase of the event loop.
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.error('promise');
});
process.nextTick(() => {
console.error('nextTick');
});Output:
nextTick
promise
timeout(5) setImmediate vs. setTimeout
setImmediate runs at the end of the poll phase (the check phase), while setTimeout runs in the timers phase after the specified delay.
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});Output:
timeout
setImmediate4. Differences Between Browser and Node.js Event Loops
Node.js: Micro‑tasks are executed between each phase of the loop.
Browser: Micro‑tasks run after the current macro‑task completes.
The article concludes with a side‑by‑side comparison of the two environments.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
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.