How does JavaScript work under the hood?

VeepeeTech
VeepeeTech
Published in
6 min readNov 23, 2022

JavaScript, explained by vpTech’s Senior Frontend Engineer, Alessandro Serafini

At Veepee, many of our micro-frontend projects were developed using React.js, a JavaScript library for creating user interfaces.

Within our frontend teams, we emphasize knowledge of the concepts behind how JavaScript works. This allows us to write optimized code with the understanding of how it will be executed.

In this article, we will then take a closer look at how JavaScript works behind the scenes.

Let’s start from the beginning: when we browse a web page, we use a web browser — Google Chrome, Firefox, Safari, etc. Each of these browsers has a JavaScript runtime.

What is JavaScript runtime?

JavaScript runtime in the browser

We can think of the JavaScript runtime as a box containing everything necessary to use JavaScript, in this case — in the browser.

We must, for example, be able to access web APIs, such as DOM, timers, Fetch APIs, etc., which are functionalities provided to the JavaScript engine. They aren’t part of the JavaScript language but are accessed via the global window object.

A JavaScript runtime also includes a callback queue: a data structure that contains all the callback functions that must be executed. This is fundamental for the Non-blocking concurrency model.

Inside the JavaScript runtime: the JavaScript engine

Inside the JavaScript runtime: the JavaScript engine

However, the heart of every JavaScript runtime is the JavaScript engine.

The JavaScript engine is a program that executes JavaScript code: every browser has its own, but the most famous is the Google V8 that powers Google Chrome and Node.js.

Every JavaScript engine within it has the following:

  • A call stack: where the JavaScript code is executed within an execution context.
  • A heap: a pool of unstructured memory containing all the objects the application needs.

Within the JavaScript engine, the conversion of the code into machine language also takes place. Usually, this transformation can be performed in two ways:

  • By compilation: the entire source code is converted at once and written in a portable file executed in the CPU.
  • By interpretation: involves an interpreter executing one line of code at a time, which is then read and executed simultaneously. Conversion takes place immediately before execution.

Transformation into machine code: JIT

The transformation of JavaScript code into machine language is done by interpretation; the problem is that it is a slower process than compiled languages. Although this was OK for JavaScript, now, with modern JS and web applications, poor performance is not allowed.

This is why the modern JS engine uses a mix of compilation and interpretation: Just-in-time compilation (JIT).

This process involves an initial parsing phase: the code is parsed into a structured Abstract Syntax Tree (AST), splitting each line of code into chunks that have meaning (const, function keyword, etc.) and storing them in a tree in a structured manner. At this stage, it is also checked for syntax errors.

The second phase is the compilation phase: the generated AST is compiled into machine language.

In the third phase, the code transformed into machine language is executed immediately: this takes place within the call stack.

Modern JS engines then utilize a clever optimization strategy: they initially create a non-optimized version of the machine code just to be executed as soon as possible. The code is then optimized in the background and recompiled while the program is running. This is done several times, and the optimized code is replaced each time without interrupting execution. This makes V8 so fast.

Parsing, compilation, and optimization occur within special threads in the JavaScript engine, which cannot be accessed by code, and are therefore separate from the main thread.

Execution context and call stack

Execution context and call stack

Let us now go into the third phase of the JIT process, namely the machine code execution phase.

We can divide this process into three further steps:

  1. Creating the global execution context: An execution context is an environment in which a piece of JavaScript code is executed. It contains all the information to execute that code: variable environment (let, const and var declarations, functions, and the ‘arguments’ keyword), scope chain, and the ‘this’ keyword.
    The global execution context is created for top-level code, which is code that is not inside functions. So initially, only the top-level code is executed.
  2. Execution of top-level code: within the global execution context. In each application, there is one and only one global execution context.
  3. Execution of functions and waiting for callbacks: When the top-level code has finished execution, the execution of functions begins: for each function called, a new execution context is created containing all the information to execute that function.

Inside the JavaScript runtime: event loop and callback queue

Inside the JavaScript runtime: event loop and callback queue

Every asynchronous event, such as DOM, timers, Fetch APIs, etc., happens in the web APIs environment. This is because if it happened at the call stack level in the JavaScript engine, it would block execution. This is why the callback queue is on the same level as the JavaScript engine and the Web APIs, i.e., in the JavaScript runtime.

When the asynchronous event has finished its execution, such as an event listener on the loading of an image, and the associated callback can be executed, it is moved by the Web APIs environment into the callback queue: an ordered list of callback functions that have to be executed.

This implies that, for example, a callback that needs to be executed after a 5-second timer is not guaranteed to be executed once the timer is over but will be inserted at the end of the callback queue after 5 seconds.

If there are already several callback functions in the callback queue that take 1 second to execute, then the timer callback will be executed after 6 seconds.

Note that DOM events, such as clicks, are also included in the callback queue, although they are not asynchronous.

At this point, the event loop comes into play, whose role is to check whether the call stack is empty (except for the global execution context): if it is, it takes the first callback from the callback queue and moves it to the call stack to be executed by the thread. This operation is called an event loop tick.

The event loop is, therefore, the one that orchestrates the JavaScript runtime. This is because the JavaScript engine has no ‘time perception’, as everything asynchronous does not happen in the JavaScript engine but executes the code it is given.

On the other hand, the callback functions of promises are not moved to the callback queue but have one dedicated queue: the microtasks queue. This takes priority over the callback queue: the event loop, when the call stack is empty, except for the global execution context, checks if there are callback functions in the microtasks queue before moving to the callback queue.

This means that as long as there are callback functions in the microtasks queue, the callback functions in the callback queue are not executed, creating a starving situation, which may be a potential problem, but, in fact is not.

--

--

VeepeeTech
VeepeeTech

VeepeeTech is one of the biggest tech communities in the retail industry in Europe. If you feel ready to compete with most of the best IT talent, join us.