Back to all articles
BackendNodejs
Inside Node.js: V8 And The Event Loop

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:

Node.js Internals

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.

Raw JavaScript Code:
let name = 'John';
function greet(n) {
  return 'Hi ' + n;
}
greet(name);
Step 1 of 9

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:

  • letkeyword token (declaration)
  • nameidentifier 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.

Node.js Event Loop Phases

Event Loop Phases: A Deep Dive

The event loop processes work in specific phases, each handling different types of tasks:

  1. Timers Phase: Executes callbacks for setTimeout and setInterval that have expired
  2. Pending Callbacks Phase: Runs certain system-level callbacks (like TCP error callbacks)
  3. Idle, Prepare Phases: Internal use by Node.js (you can't interact with these)
  4. Poll Phase: The heart of I/O handling - waits for new events and executes I/O callbacks
  5. Check Phase: Runs setImmediate callbacks
  6. 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 and Promise.catch callbacks
  • queueMicrotask() callbacks
  • process.nextTick() callbacks (Node.js specific)

Priority order:

  1. process.nextTick (highest priority)
  2. Promise callbacks and queueMicrotask
  3. 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.

Source CodeLine
  1. console.log('Start');
  2. function logA() { console.log('A') }
  3. setTimeout(() => console.log('Macrotask: setTimeout'), 0);
  4. Promise.resolve().then(() => console.log('Microtask: Promise'));
  5. process.nextTick(() => console.log('Microtask: nextTick'));
  6. console.log('End');
  7. logA();

Call Stack

Event Loop

Stack → Micro
Micro → Stack
Stack → Macro
Macro → Stack

Microtask Queue

Macrotask Queue

Output Console

    Step 0 / 37

    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 debugging
    • node --prof for CPU profiling
    • node --trace-events for detailed event tracing
    • Built-in process.memoryUsage() and process.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:

    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.
    Like our article?
    Sinister Spd