V8 is the JavaScript engine that executes code, libuv provides the event loop and handles asynchronous I/O, and together they form the core of Node.js, enabling non-blocking, event-driven server-side JavaScript execution.
Node.js is built on a layered architecture. At the top is your JavaScript application code. Below it, the Node.js runtime integrates two primary components: the V8 engine and the libuv library . V8, developed by Google, is the JavaScript engine that executes your code, handling memory management and just-in-time (JIT) compilation. Libuv is a cross-platform C library that provides the event loop and a thread pool, managing all asynchronous I/O operations like file system calls, networking, and timers . These components work together, with V8 delegating asynchronous tasks to libuv and libuv scheduling callbacks back to V8 when those tasks are complete.
V8's primary role is to execute JavaScript code. It parses the source code into an Abstract Syntax Tree (AST), compiles it to machine code using its Ignition interpreter and TurboFan optimizing compiler, and manages memory allocation and garbage collection . However, V8 itself is a pure JavaScript engine; it does not natively understand or handle I/O operations like reading files or making network requests . It provides a set of C++ APIs that allow Node.js to embed and control it, as well as to inject custom functionalities. When your JavaScript code calls fs.readFile, V8 executes the JavaScript binding for that function, which then delegates the actual operation to Node.js's native C++ layer, passing control to libuv .
Libuv is the library that gives Node.js its asynchronous, non-blocking superpowers. It is a multi-platform C library that abstracts operating system calls for I/O operations . It's responsible for the event loop, which is the central coordinator that orchestrates the execution of code . The event loop cycles through different phases (timers, I/O callbacks, idle, poll, check, close) to process different kinds of tasks . For I/O operations that are not natively asynchronous (like file system operations), libuv delegates the work to its internal thread pool (default size is 4, configurable via UV_THREADPOOL_SIZE) . Once an asynchronous operation initiated by libuv completes, its callback is placed into the appropriate queue for the event loop to execute, which will then pass control back to V8 to run the callback's JavaScript code .
It is crucial to understand that V8 does not have its own event loop. While V8 provides the call stack for executing synchronous code, the event loop is a separate mechanism provided by the runtime environment—libuv in Node.js, or the browser's Blink engine . When V8 executes an asynchronous function like setTimeout or fs.readFile, it relies on libuv to handle the timing and I/O. Libuv manages the timer or I/O operation and, upon completion, queues the callback to be executed. The event loop, which runs in the main thread, continuously checks for pending tasks in its various queues. When it finds one and the call stack is empty, it picks the next task and uses V8's C++ APIs (like v8::Function::Call) to execute the associated JavaScript callback . This coordination is what allows Node.js to handle many operations concurrently without blocking.
Consider a simple fs.readFile operation. V8 executes the JavaScript code and calls into Node.js's C++ bindings. This C++ code then requests an asynchronous file read from libuv. Libuv delegates this blocking disk operation to its thread pool. The main thread is now free to execute other code while the file is being read. Once a worker thread finishes reading the file, it notifies the main thread via libuv's internal mechanisms. The event loop, during its poll phase, receives this completion event and places the file read's callback into the appropriate queue . When the event loop later executes that callback, it uses V8 to run the JavaScript code ((err, data) => { ... }), finally passing the file data back to the original JavaScript code .