Working of JavaScript Engines

Harshit Bansal's photo
·

9 min read

Working of JavaScript Engines

As we know that JavaScript is a single threaded language and has only one call stack which means that it can perform a single task at a time. This call stack is present inside the JavaScript Engine and all the code of JavaScript is executed inside this call stack. Whatever comes into call stack gets executed instantly. We have seen the execution of programs in previous articles where we studied about how execution contexts are created and other stuff.

But if we have a program which needs to be run after five seconds. How will we do that? We cannot put the function directly into the call stack as anything inside the call stack is executed immediately. Also how can we keep the track of time that how much time is remaining to execute the function?

THIS CAN BE DONE WITH THE HELP OF THE BROWSER. BROWSER COMES UP WITH SO MANY INBUILT FEATURES WHICH WE WILL STUDY ABOUT.

The Browser has the JavaScript Engine which executes the code and it also has a Local Storage where it can store some data. Similarly, it has a Timer inside it and many other things as well like fetch, console etc. These all can be accessed with the help of Web APIs.

Web APIs

All browsers have a set of built-in Web APIs to support complex operations, and to help accessing data and developing websites. Some of the common Web APIs are given below

NOTE: setTimeout, console.log, DOM APIs, fetch are not a part of JavaScript but they are Web APIs provided by a browser. So Browser gives the accessibility of these features to a JavaScript Engine.

Now let’s try executing a code with setTimeout function.

console.log("Start");

setTimeout(function cb(){
    console.log("Callback");
}, 5000);

console.log("End");

So when we will execute this code, a global execution context will be created. Then in the first line, it will log “Start” in the console using the console API (On encountering console.log the JavaScript Engine internally call this Web API and it prints the given value on the console).

Now we will go on to the next line which is setTimeout. setTimeout will go and call its Web API and gives access to the timer feature of the browser. The setTimeout function takes a callback function and a time delay. So when the Engine encounters setTimeout function, it registers the callback function somewhere in the memory and then move on to the next line which is console.log(“End“)while the callback function timer is running in the background. When the engine executes the last line, the program ends and the Global Execution Context pops out of the call stack.

After 5 seconds, this callback function cb gets shifted from the memory into the Callback Queue and then into the Call Stack with the help of an Event Loop (Event Loop acts like a gatekeeper and it checks whether we have something in the callback quue and if we have something just like a callback function it just pushes that inside the call stack). So it pushes that callback function, execution context, and it runs this callback function line by line over here It just has one line It sees that console.log(“Callback“), it goes to the console and the console Web API actually logs it into the console and the program ends.

This is the way how a program with a time delay executes.

Microtask Queue

Similar to the callback queue, there exists one more queue which is known as the Microtask Queue. This queue has higher priority than the callback queue which means that if the call stack is empty and both microtask and callback queues contains some callback function then the event loop will first push the callback functions present in the microtask queue into the call stack and once they get executed then the functions inside the callback queue are pushed into the call stack.

The Microtask queue contains some specific callback functions like callback functions from a network, API call like fetch. Example, in this code, the callback function cb will move inside the microtask queue on completion of the network call.

fetch("https://api.github.com/users")
.then(function cb(){
    console.log("Callback");
})

JavaScript Runtime Environment

JavaScript Runtime Environment is like a container which has all the things required to run JavaScript anywhere. It contains of many things like JavaScript Engine, Callback Queue, Microtask Queue, Event Loop, Web APIs and much more. JavaScript Engine is the most important among all of them.

While the execution of a code, the code goes through three major phases :-

  1. Parsing of code

    During this phase, the code is broken down into tokens. So suppose if you write a piece of code, which is let a = 7, so this code is broken down in tokens, where let is one token, a is another token, = is another token, so something like this. And there is also something known as Syntax Parser. The job of syntax parser is to convert the code into an AST (Abstract Syntax Tree).

    An Abstract Syntax Tree (AST) is a data structure that represents the syntax of source code in a hierarchical, tree-like form. It breaks down the structure of code into its individual components, such as keywords, operators, variables, and expressions, showing how these elements relate to each other according to the language’s grammar.

    ASTs are widely used in compilers, interpreters, and tools like linters, bundlers, and code minifiers. By converting code into an AST, tools can analyze, transform, or generate code more easily. Let’s understand with the help of an example.

function foo(d) {
  d += 3;
    return d+999
}
function bar(d) {
    return d*100
}

  1. Compilation & Execution

    These both are different phases but compilation and execution is done together, JavaScript has something known as Just In Time compilation.

  2. Before we understand what is Just In Time compilation, first of all, let us understand what is an interpreter, what is a compiler, and is JavaScript an interpreted language or a compiled language?

    An Interpreter basically takes your code and it starts executing the code line by line in the order, it does not know what will happen in the next line. So that's how the whole code is executed but in the case of a Compiler, the whole code is first compiled into an optimized code, which runs very fast and it has a lot of performance improvements and then the compiled code is executed.

    In the case of Interpreter, we get more speed as it does not waits for the compilation of code to execute it, but in the case of a Compiler we get more efficiency as the compiled code is more optimized.

    JavaScript is both interpreted language and compiled language, it just depends on the JavaScript Engine.

So then the AST goes to the interpreter, then it converts our high level code to byte code and that code moves to the execution step, and while it is doing so, it takes the help of the compiler to optimize the code. So compiler basically talks to the interpreter and while the code is being interpreted line by line, the compiler also basically tries to optimize the code as much as it can. That is why it is known as Just In Time compilation. In some JavaScript Engine, there is something known as AOT Ahead of Time compilation. In that case, the compiler basically takes a piece of code, which is going to be executed later and tries to optimize it as much as it can and it also produces the byte code which then goes to the execution phase.

The two major components of the JavaScript Engine are the Call Stack and the Memory Heap. These all things works together in sync with each other to execute a program.

Memory Heap in the JavaScript Engine is the space where all the variables and functions are assigned memory. It also consists of a Garbage Collector which tries to free up memory space whenever possible, whenever some function is not being used or we clear the timeout, so it basically like collects all the garbage and clears it using an algorithm known as Mark and Sweep Algorithm.

Mark and Sweep Algorithm

The Mark and Sweep Algorithm is a garbage collection technique used in memory management to identify and reclaim memory that is no longer in use by a program. It operates in two phases: marking and sweeping.

  1. Mark Phase: In this phase, the algorithm traverses all the objects that are reachable or still in use. It marks these objects as “live” by adding them to a special list or flagging them in some way. It starts from “root” objects (like global variables, function call stacks, or other entry points) and marks all objects that can be accessed from them (directly or indirectly).

  2. Sweep Phase: After marking all the live objects, the algorithm then sweeps through the heap, looking for unmarked (unused) objects. These unmarked objects are considered garbage and are removed from memory, making space available for future allocations.

There are many other forms of optimizations which a compiler does for the code, and those optimizations are Inlining, Copy Elision, Inline Caching and a lot of other things which this compiler is doing while it is compiling the code.

Inlining is a compiler optimization technique used in programming, where a function’s code is directly substituted (or “inlined”) at the point where the function is called, instead of performing the function call. This can improve performance by reducing the overhead of function calls, especially in cases where small functions are called repeatedly.

Copy Elision is a compiler optimization technique used in C++ to eliminate unnecessary copying of objects. It allows the compiler to construct an object directly in the location where it is needed, instead of creating temporary copies and then moving or copying them.

Inline Caching is an optimization technique used in JavaScript engines (like V8, SpiderMonkey, and JavaScriptCore) to speed up property access and function calls by remembering the shape of an object and its associated method or property at runtime.


V8 Engine

At this present time, Google's V8 Engine is considered to be the fastest among the all JavaScript engines ever created. It used in many places including Chrome and Node.js. V8 has an interpreter which is known as Ignition which is a fast low-level register-based interpreter written using the backend of TurboFan.

So they have TurboFan optimizing compiler, which basically does this job of compiling the code very fast so the Ignition interpreter along with this optimizing compiler works, they both make your code run very fast. The garbage collector used by V8 Engine is named as Orinoco.

JavaScript V8 Engine. I had a great discussion with a friend ...