JavaScript under the hood

This article will illustrate the inner workings of JavaScript. It will answer questions like: Is JavaScript single-threaded or multi-threaded? If it really was single threaded, does that mean that JavaScript will run slower than code in multi-threaded languages because it could not take advantage of processing code in parallel? What is the difference between a JavaScript engine and a JavaScript runtime environment?

The big picture

Code takes a long way from the human-readable format that you write (source code) to the format that a computer can understand (machine code). It is important to understand this path to make you a better coder. As usual we will focus on JavaScript though many of the concepts described here apply to other interpreted languages as well.

JavaScript engine

In a car an engine does the bulk of work. Same applies to JavaScript: When executing your JavaScript code – no matter in which environment – it will be fed to a specific JavaScript engine. The engine is responsible for interpreting your code just-in-time, translating it to machine code and executing the machine code in a way the computer’s hardware can handle it. Just-in-time means that it does not need a precompilation phase of your code. Instead the code will be interpreted and optimized when it arrives at the engine.

Common JavaScript engines

Chrome’s JS engine is called V8, Firefox uses SpiderMonkey, Apple’s Safari uses JavaScriptCore and Microsoft’s Edge browser uses Chakra as an engine. Some engines offer more or less functionality or have their own custom way of how features are implemented, but as a common ground they implement specifications provided by a standard called ECMAScript. There are also other engines involved, such as a browser’s rendering engine which allows you to draw a graphical user interface.

JavaScript Runtime environment

Just as with a car you do not interact directly with the engine. Instead you use a pedal to accelerate the car. More abstractly, you use an interface to make the internals do their work. In JavaScript you use Application programming interfaces (APIs) that a JavaScript runtime environment provides. The JavaScript engine works inside such a runtime environment.

Common JavaScript runtimes

Several runtimes exist: You want to create a dynamic user interface in the browser? Then choose an environment that allows you to access the Document object model (DOM). You need to interact with the server’s file system? Then choose a back-end environment that provides API’s for file handling instead, such as NodeJS.

If you write code that can be executed in more than one environment without having to make environment-specific adjustments you call it isomorphic code (ancient greek for “same shape” or “equal form”).

Event Loop

Now, after this big picture overview of the way JavaScript code “travels”, we should take a closer look at how your JavaScript code is handled by a browser. Okay, this might sound crazy, but I try to illustrate the whole internal process of how JavaScript is handled within the Event Loop by using a metaphor that hopefully makes sense to everyone that has ever been at an airport:

The queue

Imagine an airport where only one single security gate is open to check people and the luggage they carry: In JavaScript there is (only) a single-threaded process running called Event Loop.

The Event Loop is automatically started when you start your browser. You do not have to program it yourself. It got its name because of how it is usually implemented by the browser developers, which looks like this:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

As you can see, the browser’s queue waits for messages to arrive – for as long as the browser is running – and then processes them. In web browsers, messages are added anytime an event occurs and there is an event listener attached to it. If there is no listener, the event is lost. Example: A click on an element with a click event handler will add a message.

myButton.addEventListener('click', function() {
  // ... do something
});

When a new traveler arrives with luggage, the person stands in line and waits until it is time to be checked by the security staff: In JavaScript a new click event message arrives that has a listener function attached. The message is put in a queue and waits to be processed by the Event Loop.

The stack frame

Now the traveler is checked, the luggage is put in the scanner. In JavaScript the message gets processed, its function A is invoked and put on a stack frame. Function A is finally executed and if it calls another function B, then function B is put on top of the stack frame and runs. In my metaphor this could be a laptop that the person forgot to put out of the backpack. It will be scanned before the backpack. After the successful scan it can be taken off the scanner, just like Function B would pop off the stack frame. The same happens with Function A until there is nothing left on the stack frame. Now the next person (message) can be processed.

Avoid blocking the Event Loop

You think it would be nice to open up more security gates so people can queue in parallel and everything is processed faster? That would work if we used multi-threaded languages like C or Java, but it does not work for JavaScript in that way.

<button id="btn">Check me</button>

<script>
    const button = document.getElementById("btn");
    button.addEventListener("click", function() {
        document.body.style.background = "green";
        const start = Date.now();
        const end = start + 10000;

        while(Date.now() < end) {
            // blocking the user interface for 10 seconds
        }
    });
</script>

What this code does: After clicking the button the user interface is completely blocked for 10 seconds. No other events will be handled. The user interface freezes. Only after 10 seconds the background of the browser document will turn green.

This is like a person trying to get through a security scanner but did forget that there are still keys in the pocket. During the 10 seconds it takes to put them aside no other people can pass the gate. Avoid executing long running code this way.

Run to completion

Let’s assume it would take even longer than 10 seconds, because that person also forgot to take out a big water bottle from the backpack (and maybe more things that trigger the scanner alert). Security staff might ask the person to step aside and re-queue, so other people can be checked. That’s what you can do with long-running threads in multi-threaded languages: You put them further back in line and let faster threads process with a higher priority. It is possible to do something like that in JavaScript using Service Workers, but for now keep in mind that once JavaScript code runs, it always runs to completion.

Example: Order of callback execution

console.log(1);

setTimeout(function() {
    console.log(3);

    setTimeout(function() {
        console.log(5);
    }, 0);

    console.log(4);
}, 0);

console.log(2);

The console logs are 1 2 3 4 5, because callback functions (such as setTimeout) are put in the execution queue only after the surrounding code ran to completion.

About Author

Mathias Bothe To my job profile

I am Mathias, born 39 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 15 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.

No comments yet.

Leave a comment