- read

How do JavaScript Runtimes Actually Work (Deno, Node, etc…)? An Intuitive Explanation

Oleks Gorpynich 75

Tools like Node, Deno, and Bun permeate the modern web development world, yet very few understand how exactly these tools work. This article will attempt to dispel some of the mystery surrounding this area through an intuitive and not overly technical explanation.

The V8 Engine

Before I jump into describing the mechanisms behind JavaScript runtimes, I must first explain the V8 engine which powers most of such runtimes. (Although there are other engines such as Apple’s JavaScriptCore)

The V8 engine is a JavaScript and WebAssembly engine developed by Google that compiles JavaScript to native machine code. It is what Chrome uses to execute client JS code and as it powers such a popular browser, the engine is optimized for speed and efficiency. Both Node and Deno use the V8 engine for code execution and in a way, these runtimes are basically a “layer” over this engine, just as Chrome is. However, Chrome takes JavaScript logic to power the webpages it displays, whereas these runtimes enable users to write JavaScript code that can function as a server.

Expected Behaviour

Before we analyze the inner workings of these runtimes, let’s first look at the expected behavior. When you run a command such as node run.js or deno run run.ts the expected result is that our run.js file is executed and some action is done. This includes

  1. Deferring any JavaScript logic inside this file to the V8 engine (which exposes an API to which you can feed JavaScript code)
  2. If there is server-specific logic (network access, file system access, etc…), the runtime needs to actually define these calls in C++(Node) or Rust (Deno) and provide usable JS wrappers for them, as the V8 engine is not built to do this. (It is for purely executing JavaScript logic)
  3. If there are any asynchronous tasks (future tasks, network calls, timers, etc…), the runtime needs to recognize these and handle them correctly, deferring the logic to the V8 engine at the right time and queuing tasks correctly.

Event Loop

Let’s tackle the last part of this first — asynchronous tasks. The way these runtimes handle these tasks is through a mechanism called the “event loop”, which is usually built into the runtime.

When asynchronous operations are invoked, tasks are added to the “task queue”. The event loop checks if the V8 engine’s call stack is empty. If it is, the event loop pushes the first task from the queue onto the call stack. V8 then executes this callback, which could again trigger more asynchronous operations, and the cycle continues. This enables most of the real world functionality that users come to expect of JavaScript code.

The Compilation

Now let’s handle the 1st and 2nd points. When you run deno run.ts a few things happen.

First, Deno binds some Rust code to a few JavaScript functions. This is because a lot of functionality lies in logic that JavaScript code cannot run by itself. For instance, any file system access or network access call has to be written as a C, C++, or Rust function first which is then binded to a JavaScript function. Deno bundles these JS definitions it created with your code, so your code is able to utilize them.

Next, any modules that you import as part of your code also have to be resolved and/or cached, which in the case of Deno are ES modules, and CommonJS modules for Node.

Transpiling if necessary also happens here. In case of Deno, JSX and TypeScript files are converted into regular JavaScript.

Finally, all this is fed into the V8 engine which exposes APIs for a lot of these things.

And that’s it! Well obviously there’s a lot more to all this. However, this covers the very basics and should allow you to look into the technicalities with more confidence.