JavaScript Engine Behind the Scenes

JavaScript Engine Behind the Scenes

How is JavaScript code able to run on so many various computing devices?

JavaScript is a high-level programming language designed to be understood by humans, as opposed to machine language (a series of 1s and 0s), which is designed for computer understanding.

Higher-level programming languages are developer-friendly, with more readable and writable syntaxes, making it easier for developers to understand, debug, and maintain the code compared to working directly with machine code.

With all that said, it’s clear computers do not understand JavaScript syntaxes the way developers do, but have you ever asked yourself how JavaScript code is able to run on so many various computing devices?

JavaScript is able to run on various computing devices with the help of a software component called the JavaScript engine.

The JavaScript engine can be found mostly in the browser or other runtime environment. In this article, we will take a closer look at how JavaScript engines work and explore some of the latest developments in JavaScript engine technology.

JavaScript Runtime Environment (JRE)

The JavaScript runtime environment is like a big box where JavaScript code is run. The box contains the JavaScript engine and some additional features (in browser context), such as the web APIs (to connect to the web), event loops, and callback queues. These features are provided so that JavaScript code can interact with the browser environment.

JavaScript runtime environment

The most common JavaScript runtime environments include:

  • The Browser runtime environment

  • The Node.js runtime environment

A browser is by far the most common environment in which JavaScript code is executed. This is because JavaScript was originally designed to enhance interactivity in front-end applications. However, the Node.js runtime environment was also introduced for the purpose of executing JavaScript code outside the browser and creating server-side applications, APIs, and backend servers.

JavaScript Engine

A JavaScript engine is a program that interprets JavaScript code, translating it into machine code.

JavaScript is a single-threaded programming language that can only run one task at a time. The JS engine can get blocked in the process of running heavy tasks that take a long time to execute because of it’s single-threaded nature. However, JavaScript provides a way of handling such tasks using some of the asynchronous techniques like Promise, async/await, and callbacks, which enable the code to keep running without blocking the main thread.

The most common JavaScript engines are:

  • V8 engine (used in Chrome and Node.js)

  • SpiderMonkey (used in Firefox)

  • Chakra (used in Microsoft Edge)

  • JavaScriptCore (used in Safari )

The JavaScript engine is made up of two components, namely:

  • Call stack

  • Memory heap

JavaScript engine

The call stack and memory heap are both used to manage data storage and the execution of JavaScript programs, but they serve different purposes.

The call stack manages the order of currently executing function calls, while the memory heap stores dynamic data that is not currently being used, such as arrays, objects, and other complex data structures.

As previously mentioned, a JavaScript engine is a program that interprets and translates JavaScript code into a machine-understandable language. The JS engine begins by running the code through a parser.

PARSER

Parser stage before code execution.

The parser is the first stage of the JS engine. It is responsible for breaking down the JavaScript code into smaller units called tokens (usually keywords, operators, and identifiers). After that, the parser checks for syntax errors on the sequence of tokens generated; if the parser finds an error, the JS engine stops execution of the code and throws a kind of error message. However, if the parser finds no errors, it produces what is called an abstract syntax tree (AST).

Abstract Syntax Tree (AST)

AST stage before code execution

AST is a tree representation of your code syntax, making it easier for the JavaScript engine to understand, analyze, optimize, and execute the code syntax efficiently.

You can check AST Explorer to see what the actual AST looks like. It is an online tool that generates the AST for any code that you enter.

Interpreter

Interpreter stage before code execution

The interpreter operates by converting the AST that is generated during the parsing stage into an intermediate representation (IR) before the code is finally executed.

NB: JavaScript engines use a variety of strategies to execute JavaScript code efficiently in various execution environments. For instance, SpiderMonkey, which was the first JS engine, uses an interpreter to execute JavaScript code; however, some of the most modern JS engines, like the V8 engine, use techniques such as IR, just-in-time (JIT), and so on to achieve very high performance, making the V8 engine one of the fastest JavaScript engines available.

The Intermediate Representation (IR)

IR is a representation of a program’s source code; IR acts as an intermediate step between the AST-generated code and machine code.

Bytecode is an example of IR. It acts as a higher-level representation of machine code, which means bytecode is not tied to any machine architecture. As a result, bytecode can be executed on different platforms without the need for recompilation.

Why is an intermediate representation (IR) needed in a JavaScript engine?

IR is needed so that JavaScript code can be portable and run efficiently on every platform; without IR code, you may need to recompile every line of JavaScript code whenever you transfer it between platforms. However, IR made it possible for code written on machine A to run on machine B without the need for recompilation because IR is not bound to a specific machine architecture.

Compiler

Compiling stage

The major work of the compiler is to take the IR code and then transform it into more optimized machine code.

Interpreter vs Compiler

Compilers and interpreters are two different programs used to translate higher-level languages (source code) into machine code that computers can understand.

An interpreter is a program that reads and translates source code one line at a time and then executes the code before moving to the next line for execution.

A compiler, on the other hand, is a program that instantly reads and translates the entire source code into machine code or an intermediate representation (bytecode) in one go. The compiled code is then executed directly by the computer.

Interpreter pros and cons

The interpreter takes less time to translate the source code; this is because the interpreter does not create an intermediate compiled version of the entire program before execution. The interpreter reads and executes the code line by line, and so on.

However, the con of using an interpreter comes when you run the same code repeatedly. For instance, if you’re in a loop, you have to retranslate the same code repeatedly. This makes the overall execution time of the program slow.

Compiler pros and cons

In contrast to the interpreter:

The compiler process is slow at the start because the program has to first translate the entire source code into machine code, or IR, before the code is finally executed by the computer. However, the execution is very fast because it doesn’t have to retranslate the same code repeatedly, e.g., if a code is running through a loop.

Additionally, during the compilation stage, the program type-checks for errors and makes edits to the code. These edits are known as optimizations.

These optimizations make the execution of the program really fast.

Just-in-time (JIT) Compilation

The basic idea of a just-in-time compiler is to avoid the need to repeatedly translate the same code where possible and to improve the overall performance of JavaScript in web browsers.

JIT combines the best of both the interpreter and compiler methods, which significantly improves the performance of JavaScript code.

Just-in-time in JavaScript refers to the process whereby the compilation of code is done at runtime (meaning it is done during the execution of the program), as opposed to ahead of time (AOT), where the code compilation takes place first before execution.

Monitor

Generally, every browser has its own unique way of implementing its own version of a just-in-time compiler. But the basic idea is often the same across all browsers. They added a new component to the JavaScript engine called the Profiler, also referred to as a Monitor. The monitor’s job is to keep track of how code is run, how many times it is run, and what types are used.

To start, Monitor runs a code through the interpreter; this is because interpreters are quick to start things up. While the code is being interpreted, the monitor identifies code that runs a few times (the warm code segment) and code that runs many times (the hot code segment), then stores such code in the memory for another execution so that next time the JavaScript engine comes across the same method of code in the program, it executes directly from the memory instead of having to recompile the original source code.

below is a breakdown with an example

To execute the provided code, just-in-time parses the code and then generates baseline instructions for the function. The baseline instructions are relatively simple to make the code start quickly. As the code runs, JIT gathers information such as the array’s length and the types of operations performed. These instructions are needed by the compiler to decide if to generate an optimized version of the code or to continue the execution using the baseline instructions.

The compiler generates optimized machine code if it notices the array’s length doesn’t change during the loop; however, if the array length changes and still maintains the same data types, JIT recompiles the code using a more optimized version of the code, which significantly improves the performance of the loop.

Optimizing compiler

Basically, an optimizing compiler gathers information from the interpreter before making assumptions, which will then be used to create an optimized version of the code. For instance, the compiler can assume the types of variables and the order in which properties appear are going to be the same, and so much more.

However, the assumptions of the optimizing compiler may turn out to be wrong if there is a change in the code, most likely in the variable types. The optimizing compiler will then perform what is called deoptimization and generate new machine code (not so efficient code).

Deoptimization can occur for various reasons, but in our own case, the optimizing compiler can assume the array is always a number. However, if the array changes to a string, it may trigger deoptimization and then generate new machine code that is not so efficient.

Summary

  1. JavaScript code is run in certain environments, the most common being the browser and Node.js runtime environments. The environment consists of the JavaScript engine to run JavaScript code and some additional features that enable JavaScript code to interact with its surroundings.

  2. The JavaScript engine is the program that interprets and translates JavaScript code into machine-understandable language. It started by first running the code through a parser, which breaks the code into smaller units called tokens. Afterward, an AST is generated.

  3. The interpreter is responsible for converting AST code into an intermediate representation (IR), such as bytecode, which acts as a higher-level form of machine code.

  4. The JIT compiler takes the IR (bytecode) and compiles it into machine code, later generating a more optimized version of the code from the hot segment, which significantly improves the code’s performance.

Relevant References:

How JS Works Behind The Scenes — The Engine

A crash course in just-in-time (JIT) compilers

How Does the JavaScript Engine Work?