Inside Node.js: V8 And The Event Loop
Ever wondered what happens after you type node app.js
? In this comprehensive guide, we'll dive deep into Node.js internals and explore how V8 parses your JavaScript into an AST, runs it through Ignition and TurboFan, and how Node's event loop (powered by libuv) handles asynchronous operations like promises, timers, and I/O.
Understanding JavaScript Execution
A computer, a compiler, or even a browser can't actually understand code that's written in JavaScript.
If so, how does the code run?
Behind the scenes, JavaScript always runs inside a runtime environment.
The most common ones are:
- Browser → by far the most common, where JavaScript powers interactivity in websites.
- Node.js → a runtime environment that allows you to run JavaScript outside of the browser, usually on servers.
Here's a general overview of how a JS engine works:

V8 Execution Pipeline
Use this interactive visualizer to watch how your code works behind the scenes in real-time:
Step 1 – Source Code
When you run node app.js
, Node.js opens the file and reads the JavaScript code as plain text. At this step, the code is still just characters . Node hasn't tried to break it down into tokens or instructions yet.
let name = 'John';
function greet(n) {
return 'Hi ' + n;
}
greet(name);
1) Tokenization: Breaking Code into Pieces
Your source code is first broken into tiny pieces called tokens. Think of tokens like words and punctuation in a sentence. This makes the code easier to analyze and process systematically.
let name = "Sinister";
// Tokens: let | name | = | "Sinister" | ;
The tokenizer breaks this into distinct tokens:
let
→ keyword token (declaration)name
→ identifier token (variable name)=
→ assignment operator"Sinister"
→ string literal;
→ semicolon (statement terminator)
Why tokenization matters: This step creates a standardized representation that the parser can understand, regardless of how you format your code (spacing, line breaks, etc.).
2) Parsing to AST: Building the Code Tree
- Tokens are transformed into an Abstract Syntax Tree (AST). An AST is a hierarchical data structure that represents the logical structure of your code. This concept isn't unique to JavaScript - it's used by many programming languages including Java, C#, Ruby, and Python.
Key benefits of AST creation:
- Easier optimization: Tree structures are easier to analyze and optimize
- Language-agnostic: The same parsing techniques work across different languages
- Better error handling: Syntax errors can be caught and reported more effectively
You can actually explore how the AST looks for any code using the excellent AST Explorer tool. Simply paste your JavaScript code and see the tree structure that gets created.
3) Ignition: The Fast Interpreter
V8's interpreter is called Ignition. It converts the AST into bytecode and starts executing it immediately. This gives you fast startup times without waiting for heavy optimization.
function add(a, b) {
return a + b;
}
add(2, 3);
How Ignition works:
- Converts AST to bytecode (platform-independent intermediate representation)
- Executes bytecode using a virtual machine
- Provides fast execution for code that runs only a few times
- Collects runtime information for optimization decisions
4) Hot Code Detection and TurboFan Optimization
When a function runs many times, V8 marks it as "hot" and sends it to TurboFan, the optimizing compiler. TurboFan creates a faster machine-code version based on what it learned about the values and types during execution.
Optimization strategies:
- Type specialization: Creates fast paths for specific data types
- Inlining: Embeds small function calls directly into the calling function
- Dead code elimination: Removes unreachable code paths
- Loop optimization: Unrolls and vectorizes loops where beneficial
Important: Optimizations rely on assumptions about your data. If your data changes shape (e.g., a function that always received strings suddenly gets a number), V8 may throw away the optimized code and fall back to the safe, slower version. This is called deoptimization and can cause performance hiccups.
The Event Loop: Node.js's Beating Heart
Node.js runs JavaScript on a single main thread, but it's far from single-threaded in practice. The event loop orchestrates when timers fire, when I/O callbacks run, and when immediate tasks or close handlers execute. This is what makes Node.js so efficient for I/O-heavy applications.

Event Loop Phases: A Deep Dive
The event loop processes work in specific phases, each handling different types of tasks:
- Timers Phase: Executes callbacks for
setTimeout
andsetInterval
that have expired - Pending Callbacks Phase: Runs certain system-level callbacks (like TCP error callbacks)
- Idle, Prepare Phases: Internal use by Node.js (you can't interact with these)
- Poll Phase: The heart of I/O handling - waits for new events and executes I/O callbacks
- Check Phase: Runs
setImmediate
callbacks - Close Callbacks Phase: Executes close events like
socket.on('close')
Microtasks: The High-Priority Queue
Microtasks run between turns of the event loop and after each phase step. In Node.js, microtasks include:
Promise.then
andPromise.catch
callbacksqueueMicrotask()
callbacksprocess.nextTick()
callbacks (Node.js specific)
Priority order:
process.nextTick
(highest priority)Promise
callbacks andqueueMicrotask
- Regular event loop phases
console.log("Start");
// Macro task (event loop phase)
setTimeout(() => {
console.log("setTimeout callback");
}, 0);
// Micro task (runs between event loop phases)
Promise.resolve().then(() => {
console.log("Promise then callback");
});
// process.nextTick (Node.js specific, highest priority microtask)
process.nextTick(() => {
console.log("process.nextTick callback");
});
console.log("End");
Warning: process.nextTick
can starve the event loop if overused because
it always runs before other tasks. This can prevent I/O operations from
completing. Prefer Promises for most microtask work, and use
process.nextTick
sparingly for cleanup or error handling.
Interactive Event Loop Visualizer
Now that you understand the theory, let's see it in action! Use the interactive visualizer below to step through how the event loop executes code. Watch how tasks move between the Call Stack, Microtask Queue, and Macrotask Queue in real-time.
console.log('Start');
function logA() { console.log('A') }
setTimeout(() => console.log('Macrotask: setTimeout'), 0);
Promise.resolve().then(() => console.log('Microtask: Promise'));
process.nextTick(() => console.log('Microtask: nextTick'));
console.log('End');
logA();
Call Stack
Event Loop
Microtask Queue
Macrotask Queue
Output Console
How Node.js Handles Asynchronous Work
JavaScript execution is single-threaded, but the platform around it is not. Node.js relies on libuv (a cross-platform asynchronous I/O library) to handle I/O operations efficiently without blocking the main thread.
libuv and the Thread Pool
Some operations (like filesystem access or cryptographic operations) cannot run asynchronously at the operating system level. For these cases, libuv provides a thread pool - a small pool of worker threads that can execute these blocking operations.
const fs = require("fs");
const crypto = require("crypto");
// These operations use the thread pool
fs.readFile("file.txt", () => console.log("file done"));
crypto.pbkdf2("x", "y", 1e5, 32, "sha256", () => console.log("crypto done"));
Thread pool characteristics:
- Default size: 4 threads (configurable via
UV_THREADPOOL_SIZE
) - Ideal for: File I/O, crypto operations, DNS lookups
- Not suitable for: CPU-intensive JavaScript operations
Performance tip: Only increase the thread pool size when you have evidence of I/O queueing (long wait times for file operations). A larger pool can help with file and crypto tasks, but it will not speed up JavaScript execution on the main thread. Monitor your application's performance metrics before making changes.
Worker Threads: True Parallelism for CPU-Intensive Tasks
For CPU-heavy tasks that would block the main thread, Node.js provides Worker Threads. Each worker runs in its own thread with its own event loop and memory space, enabling true parallel execution.
When to use Worker Threads:
- CPU-intensive computations: Mathematical calculations, data processing
- Image/video processing: Resizing, filtering, format conversion
- Complex algorithms: Machine learning inference, cryptography
- Heavy transformations: Large data parsing, compression
// main.js - Main thread
const { Worker } = require("worker_threads");
// Create a new worker thread
const worker = new Worker("./worker.js");
// Listen for results
worker.on("message", (result) => {
console.log("Worker completed:", result);
});
// worker.js - Worker thread
const { parentPort } = require("worker_threads");
// CPU-intensive work
let sum = 0;
for (let i = 0; i < 5e7; i++) {
sum += i;
}
// Send result back to main thread
parentPort.postMessage(sum);
Important distinction: Worker Threads are ideal for CPU-bound computations (image processing, cryptography, complex calculations). For I/O operations (file reading, network requests, database queries), the event loop + libuv is usually more efficient and easier to manage.
Performance Monitoring and Debugging
Understanding Node.js internals helps you write better code and debug performance issues more effectively.
Key metrics to monitor:
- Event loop lag: Time spent in each phase
- Memory usage: Heap and external memory consumption
- CPU usage: Main thread and worker thread utilization
- I/O wait times: File and network operation delays
Debugging tools:
--inspect
flag for Chrome DevTools debuggingnode --prof
for CPU profilingnode --trace-events
for detailed event tracing- Built-in
process.memoryUsage()
andprocess.cpuUsage()
Key Takeaways
- V8 execution pipeline: Code → Tokens → AST → Bytecode → Optimized Machine Code
- Event loop phases: Timers → Pending → Idle/Prepare → Poll → Check → Close
- Microtask priority:
process.nextTick
> Promises > Event loop phases - libuv thread pool: Handles blocking I/O operations (files, crypto, DNS)
- Worker Threads: Use for CPU-intensive tasks, not I/O operations
- Performance: Monitor event loop lag, memory usage, and I/O wait times
Further Learning
To dive deeper into Node.js internals, explore:
- V8's official documentation
- libuv documentation
- Node.js performance best practices
- Event loop visualization tools
Understanding these internals will make you a better Node.js developer and help you write more performant, maintainable applications.
Have feedback?
- Was anything confusing, hard to follow, or out of date? Let me know what you think of the article and I'll make sure to update it with your advice.
Read more

Tanstack Query
Understand what TanStack Query is, why it’s powerful, and how to start using it in your projects.

Build Your Own Custom Event Emitter
Ever wondered how EventEmitter in Node.js really works under the hood? In this beginner-friendly guide, we’ll build our own custom Event Emitter from scratch in JavaScript, step by step.