JavaScript Runtime and Code Lifecycle
Tags: javascript, interview, architecture, webdev
Published: 2023-08-17 09:10:44



In this article we will explore all the important aspects of JavaScript's runtime. Our aim is to understand the general inner workings of JavaScript's runtime, covering all the important details. Let's delve in!

&nbsp; 
 
## Architecture Overview

JavaScript is a dynamic, single-threaded, JIT compiled, stack-oriented programming language. I have covered technical details of the language, in my other article: [Technical introduction to JavaScript](https://dev.to/odus_ex/a-short-technical-introduction-to-javascript-13a0). It is important to understand these aspects beforehand to get the most of this article.

&nbsp; 

Lets start with a general architectural diagram of the browser runtime:


![JavaScript Runtime Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/67ept4u8h6t4mcib6l9d.png)


In the above diagram, the colourful part is the core JavaScript. Everything else is the environment which is responsible to provide a platform to run JavaScript code successfully. 

Anytime we are running a JavaScript code, we are running it in some kind of environment, which is not a part of the language specification. A JavaScript Environment is a system that facilitates JavaScript code execution using an ***engine*** and extends JavaScript's functionality by providing different functions and APIs. For eg:

- *NodeJS* is a JavaScript environment which use a JavaScript engine called ***V8*** and provide APIs to create HTTP servers, handle file management and much more. 

- *Chromium* based browsers (like *Google Chrome*) are JavaScript environments which use V8 engine, same as *NodeJS*, and provide Web APIs (`document`, `window`, etc) to perform front-end specific task for creating powerful dynamic websites.

Other famous browsers have their own implementations of Web APIs and engines. For eg. *Microsoft's Edge* uses *Chakra*, *Mozilla's Firefox* uses *SpiderMonkey*, etc.

- Some IoT platforms use [Duktape](https://duktape.org/) and [JerryScript](https://jerryscript.net/) as their engines.



## JavaScript Code Execution

JavaScript is a single-threaded language, which means it can only execute one task at a time. When a program is executed, the function being executed are called one by one for the execution. To keep track of the execution status and function calls, JavaScript utilise a stack data-structure called call-stack. To understand it better, lets consider an example:

```js

function multiply(x,y){
  return x * y;
}

function square(n){
  return multiply(n, n)
}

let a = 10;

console.log(square(a))

```

When the above code is run, each function call pushes the function *being called* into the call-stack. When the function has completed its execution, they are popped from the call-stack.


![JavaScript call-stack for synchronous code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mupjkagzll9pkz131jtd.png)

The above code is executed synchronously, meaning all the execution happens immediately, in one go, at a given point of time, without the control shifting over to some other entity. To contrast, lets modify our example to understand asynchronous code behaviour.

> We can develop asynchronous code using JavaScript because of the ***Event Loop*** and ***Call-back Queue***, provided by the runtime environment (see the architecture diagram above).  These features provided by JavaScript runtimes enable use to write asynchronous code. JavaScript by itself is a blocking synchronous language.

In the code below, we use a function from Web APIs called `setTimeout` to create asynchronous behaviour. It enables us to call a function (passed as first parameter to `setTimeout`) after a mentioned time in milliseconds (second parameter) hence making the code asynchronous.

```js
function multiply(x,y){
  return x * y;
}

function square(n){
  return multiply(n, n);
}

let a = 10;

setTimeout(function later(){console.log(square(a))}, 1000);

console.log('Hi')

``` 

Upon execution, the function `later` is send to the call-back queue, to ***call it back*** after `1000` milliseconds, or one second. That is why, when we run the code, we first see `Hi` and then, at least after a second, `100` in the output. When the code is getting interpreted, the asynchronous functions are send to the callback queue. These are pushed to stack once the call-stack is empty, this is done by ***Event Loop***. 


![JavaScrip call-stack for asynchronous code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8kk2186c00cz0wdfis32.png)


By now we have a general idea of how JavaScript runtime works in synchronous and asynchronous operations. If you are interested in further explorations [JS 900](https://www.jsv9000.app/) is a great tool to explore synchronous and asynchronous language behaviour in-depth. 


&nbsp;
&nbsp;



## JavaScript Code Lifecycle

Let us go one level deeper in understanding how the code being executed is transformed end-to-end i.e. starting from source file all the way to generating machine code and executing it.

When we run a JS program in any environment discussed above, it goes to different phases before it is finally executed. These are namely *Tokenisation*, *Parsing*, *Code-Generation* and finally *Code-Execution*. 

![JavaScript Code LifeCycle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9burtpexzv35f0yhep1.png)



> Novice developers often claim that JavaScript is an interpreted language, but as we will see below, this is only half-truth.

Any JavaScript program is always tokenised and parsed to generate an intermediate form of code called *byte-code* which is then interpreted by an *Interpreter* to generate executable *machine code*. This is different from purely interpreted languages where source code is directly converted to machine code for execution. ([Difference between Byte Code and Machine Code](https://www.geeksforgeeks.org/difference-between-byte-code-and-machine-code/))

A JavaScript program is dynamically compiled to byte-code, before it is interpreted and executed. This is why JavaScript is a 'Just-in-time' (JIT) compiled language. Notice that we used the term *dynamically* compiled. This is to reflect on the fact that JavaScript Engine doesn't have the luxury to pre-compile JavaScript and run it later (also called *static* compilation or *Ahead-of-Time* (AOT) compilation) like other compiled languages. Lets understand this whole jargon step-by-step in detail now. To set the premise, the JavaScript runtime under consideration is V8, used by Chromium based browsers and NodeJS.


We start with a sample JavaScript source code.

```js
var sayHello = 'Hello World!';

function printMessage(message){
  	console.log(message)
}

printMessage(sayHello)
```

The engine starts with locating the source code. Generally, this code will either be on a file, in a Web Server or present in a script embedded in an HTML `<script>` tag. 

&nbsp;

### Tokenisation
After identification, the source code is tokenised. The process is called ***tokenisation***. During the process, JavaScript engine converts syntactically sound source code into *tokens*. This is the process where error in syntax are highlighted. In very simple terms, the source code is broken down into smaller valid language constructs. For eg. `var sayHello = 'Hello World!';` is broken down into `var` a keyword to declare functional scope variables, `sayHello` an identifier, `=` an operator and `'Hello World'` a string literal and finally `;` a punctuator.

> In some compiled languages, this process is also called *lexing*.

&nbsp;

###Parsing
Once the tokens are generated, they are passed to a ***Parser*** as input, which performs *parsing*. Parsing is the process to generate Abstract Syntax Tree ([AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree)) and execution scopes. Generating AST allow us to represent the hierarchal structure of the program. This hierarchy information is used by Interpreter in subsequent steps for generating byte-code.



![JavaScript AST](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sz2d3guq41pbjloeltjq.png)


Notice that AST doesn't directly contain the value of the literals or variables. We will cover this later. 

&nbsp;

For V8 engine, there are two types of parsing: *eager parsing* and *lazy parsing*. The eager parsing generates both AST and execution scopes, while lazy parsing only generates AST.

> Execution scopes, in simple terms are places which map identifiers to their assigned value. For our example `a` is mapped to `10` number literal. This information is stored in execution scopes, instead of directly putting them in AST, to keep the AST generation a fast process. 


Eager parsing is used for top level code, which will be executed immediately by the Interpreter while lazy parsing is for the nested code and declarations which are not called and hence are not required for the execution of the program yet.

 ```js
var sayHello = 'Hello World!'; // eagerly parsed

// eagerly parsed, but everything inside is lazy parsed for now
function printMessage(message){ 
  	console.log(message)
}

// oh shit, need to parse everything eagerly inside, as the function is called.
printMessage(sayHello) 
```


In eager parsing, the generated execution scopes are used to assign identifier values to *variable proxies* inside AST (as mentioned in the diagram above), which did not have any values assigned to them while AST generation. Lazy parsed AST are left without assigning the identifier values so that they are created faster and hence speed up the parsing process. The process of assigning identifier value from execution scopes to variable proxies in AST is referred to as *Scope Analysis*.


> Lazy parsing is performed by a *pre-parser* according to the V8 terminology. Pre-parsers are twice as fast as Parsers because they skip execution scope generation and value assignments.

&nbsp;

### Code-Generation & Execution

Once the AST is generated and hydrated (variables proxies assigned with values), it is passed to a *Byte-code Generator*. It is responsible for producing a stream of byte-code, one byte-code at a time. Once the byte-code is generated, it is finally interpreted by an Interpreter to produce *machine Code* (0s and 1s). The machine code is then executed to produce the program output.

&nbsp;
&nbsp;


### A word on Javascript Optimisations

As a side note, it is a good option to discuss some techniques like *inline caching* adopted by JavaScript engines to speed up the code compilation and execution process. JavaScript engines are capable of optimising code runs when a piece of code is being run multiple times.

 As we have already discussed above, once the hydrated AST is fed to an Interpreter, it starts generating a stream of byte-code. While it is doing this, the output stream is passed to an *'optimising compiler'*  which is responsible for producing an optimised version of machine code, depending on what information it has from previous execution runs. Below is a very generic diagram of the process. The whole process is referred to as *execution & optimisation pipeline*. 


![JavaScript Optimisation Pipeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/75fqc3qeaqgewgx8fc0m.png)



&nbsp;

When the Interpreter is generating byte-code, it also stores some sort of *profiling data*, which can be used in subsequent runs. When a function becomes *'hot'* (i.e. when it has been run multiple times), the byte-code generated for that function, along with the profiling data is passed to the optimising compiler to create *'inline cache'* for that function, which on future runs, are directly swapped in the byte-code stream instead of regenerating the byte-code. 

> Some engines have multiple optimising compilers instead of just one. Essentially all optimisations and caching techniques function on the fact that JavaScript engine is able to *'profile'* the *Object Shape* (everything non-primitive in JavaScript is an Object). The article: [JavaScript Shapes and Inline Caches](https://mathiasbynens.be/notes/shapes-ics) explains the whole optimisation process in detail.


&nbsp;
&nbsp;

### Conclusion

Our journey into JavaScript's runtime has unveiled its core mechanics, from architecture to code execution and lifecycle. We've grasped its dynamic, single-threaded nature, role of environments, and execution contexts. Understanding both synchronous and asynchronous code execution, the event loop's role, and diving into the lifecycle from source to machine code, we've unraveled JavaScript's essence as a "Just-in-time" compiled language. Further, optimisation techniques like inline caching have been explored, showcasing the intricate strategies boosting performance. 

Until next time :)