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? 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). 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 pre-compilation phase of your code. Instead the code will be interpreted and optimized when it arrives at the engine.

flowchart LR id1["JS source code"] --> id2[JS engine]-- JiT interpretation ---id3[machine code]

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 speaking: 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 runtime environment includes everything needed to run JavaScript code, such as the engine, memory heap, call stack, event loop, and various APIs.

graph TD subgraph JavaScript Runtime B(JavaScript Engine) C(Memory Heap) D(Call Stack) E(Callback Queue) F(Event Loop) end subgraph BrowserAPIs G(DOM) H(XMLHttpRequest) I(Timers) J(Web APIs) end

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.

Event Loop

Now, after this big picture overview of the way JavaScript code "travels", we should take a closer look at how JavaScript code is handled by a browser.

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. It got its name because of how it is usually implemented by the browser developers, which looks like this:

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

Different queues

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, when the user clicks mybutton, a 'click' event is triggered. The event is placed in the event queue. The event loop continuously checks the event queue for any events. When it finds the 'click' event in the queue, it takes the associated callback function and adds it to the call stack for execution.

  1. Task Queue:
    • Content: The task queue is a general term that encompasses various types of tasks, including script execution, I/O operations, user interactions, and other events.
    • Order of Execution: Tasks in the task queue are processed in a first-in, first-out (FIFO) order.
    • Examples: Script execution, timers (setTimeout, setInterval), DOM events, and other asynchronous operations contribute tasks to the task queue.
  2. Callback Queue (or Message Queue):
    • Content: The callback queue is a specific type of queue within the task queue. It holds callbacks that are scheduled to be executed after the completion of the current task or execution stack.
    • Order of Execution: Callbacks in the callback queue are processed in a first-in, first-out (FIFO) order when the call stack is empty and the event loop is ready to pick the next task.
    • Examples: Callbacks from asynchronous operations like timers, event handlers, and AJAX callbacks are placed in the callback queue.
  3. Microtask Queue:
    • Content: The microtask queue holds tasks that are typically higher-priority and execute before the next task from the callback queue.
    • Order of Execution: Microtasks are executed after the current task but before the next task from the callback queue. They have a higher priority than the callback queue.
    • Examples: Promises and certain APIs like MutationObserver, and process.nextTick in Node.js schedule tasks that are placed in the microtask queue.

    Call stack

    The call stack is a data structure that tracks the execution of a program. It keeps track of the currently executing functions and their order of execution. As functions are called, their execution context (including local variables and parameters) is pushed onto the call stack. When a function completes, its execution context is popped off the stack. The call stack follows the Last In, First Out (LIFO) principle, meaning the last function added to the stack is the first one to be removed.

    Stack frame

    A stack frame is a specific part of the call stack that corresponds to a single function call. It contains information about the function's execution, including local variables, parameters, and the return address. Each function call pushes a new stack frame onto the call stack. When the function completes, its stack frame is popped off the stack. The stack frame is essential for maintaining the state of the function and allows the program to return to the correct point after the function call is complete.

    graph TD A(Call Stack) B(Main Function) C(Function 1) D(Function 2) E(Function 3) A --> B B -->|Calls| C C -->|Calls| D D -->|Calls| E subgraph A[Call Stack] F1[Execution Context 1] F2[Execution Context 2] F3[Execution Context 3] end B --> F1 C --> F2 D --> F3

    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.

    Heap

    The heap in JavaScript is a region of memory where dynamic memory allocation occurs. It is used to store objects and variables that are created during the runtime of a program. Unlike the call stack, which is a more limited and structured memory space for function calls and local variables, the heap is a larger and less structured memory area for dynamic memory allocation.

    1. Object Storage:
      • The heap is primarily used to store objects and variables that are created dynamically during the execution of a program.
      • Objects like arrays, functions, and custom objects are allocated memory on the heap.
    2. Dynamic Memory Allocation:
      • Unlike primitive data types (numbers, strings, booleans) that are stored in the stack, objects in JavaScript often have dynamic sizes.
      • The heap allows for flexible memory allocation to accommodate varying sizes of objects.
    3. Reference Management:
      • Objects in the heap are accessed through references. Variables in the stack contain references to objects in the heap.
      • Memory management, including allocation and deallocation, is handled automatically by the JavaScript engine's garbage collector.
    // Creating objects in the heap
    let obj1 = { name: 'John', age: 30 };
    let obj2 = [1, 2, 3, 4];
    
    // Creating a function in the heap
    function greet(person) {
      console.log(`Hello, ${person.name}!`);
    }
    
    // Creating an array in the heap
    let arr = [5, 10, 15];
    
    // The heap is used for dynamic memory allocation for objects and arrays
    

    About Author

    Mathias Bothe To my job profile

    I am Mathias, born 40 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 16 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