Single-Threaded Nature
JavaScript is a single-threaded language, meaning it has one Call Stack and can execute only one command at a time. However, modern web applications handle multiple network requests, user interactions, and timers simultaneously. This is achieved through the Concurrency Model and the Event Loop, which allow JavaScript to perform non-blocking I/O even while running on a single thread.
The Components of the Runtime
The JavaScript runtime is composed of several key parts:
- Memory Heap: Where memory allocation happens (objects, variables).
- Call Stack: Where the engine keeps track of function execution (LIFO - Last In, First Out).
- Web APIs / Node APIs: External environments (provided by the browser or Node.js) that handle things like
setTimeout, DOM events, and fetch requests. - Callback Queue (Task Queue): Where callbacks from Web APIs wait to be pushed onto the stack.
- Microtask Queue: A high-priority queue for Promises and
process.nextTick.
The Mechanics of the Event Loop
The Event Loop has a very simple job: monitor the Call Stack and the Task Queues. If the Call Stack is empty, it takes the first task from the queue and pushes it onto the stack, which effectively runs it.
The Execution Order
- Execute all synchronous code in the Call Stack.
- Check the Microtask Queue. Execute all pending microtasks until the queue is empty.
- Check the Callback Queue (Macrotasks). Execute the first task in the queue.
- Repeat.
This means that microtasks (like Promise resolutions) are always executed before the next macrotask (like setTimeout), even if the macrotask was scheduled first.
Blocking the Event Loop
Since the Event Loop can only process one thing at a time, performing a heavy computation (like calculating prime numbers or processing a large image) synchronously will “block” the event loop. While the loop is blocked, the browser cannot render updates, handle user input, or process callbacks. This results in a “frozen” user interface.
Visualizing Asynchrony
Consider the following code:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
Output sequence:
Start(Synchronous)End(Synchronous)Promise(Microtask - higher priority)Timeout(Macrotask - lower priority)
Even though setTimeout has a delay of 0 ms, its callback is placed in the Macrotask queue, which must wait for the current execution and the entire Microtask queue to finish.