How JavaScript Works Behind The Scenes?

How JavaScript Works Behind The Scenes?

An overview of How JavaScript Works Behind The Scenes

·

15 min read

Introduction

The diverse JavaScript ecosystem helps us build fast and scalable software using different approaches. Numerous components behind the scenes make JavaScript more efficient and flexible. Understanding how JS works benefits us greatly when we're building software systems or want to dive deeper into the JavaScript world.

This article will give you a partial overview of how JS components work; it not only provides you with a place to start, but you can then leverage it to dive even deeper. It covers main components such as JavaScript Runtime, JavaScript Engine, Parser, Compiler, Interpreter, Garbage Collectors, Event Loop, etc.

I hope you enjoy this article and don't hesitate to drop your comment or correct me if anything's wrong.

What is JS Runtime?

💡
runtime is the environment in which a programming language executes. It allows the programming to interact with the computing resources.

The JavaScript runtime is a program that extends the JavaScript engine and provides access to built-in libraries and additional functionalities, so it can interact with the outside world and make the code work. Besides that, it facilitates storing functions, and variables, and managing memory by using data structures such as queues, heaps, and stacks.

The different browser has their runtime such as V8 for Chrome, WebKit/JavaScriptCore for Safari, and SpiderMonkey for Firefox. This environment has several common components:

  • The JavaScript Engine (which includes a Call Stack and Memory Heap).

  • Web APIs (including the DOM and Web Storage).

  • Queues (Tasks and Microtasks).

  • The Event Loop.

What is JS Engine?

The JavaScript Engine is typically associated with executing JS code and accessing behavior contained in .js files. It will translate source code into machine code that allows a computer to perform specific tasks.

It’s single-threaded, which means that It can only consecutively parse code line-by-line, and can be blocked if it encounters an endless loop or gets into some infinite subroutine

This engine has 6 main components: Parser, Interpreter, Compiler, Call Stack, Memory Heap, and Garbage Collectors.

6 JS Engine Components

Overview of JS Engine

Parser

💡
A parser is a computer program which is a part of the compiler. It’s responsible for breaking the source code into small elements (units) such as expressions, statements, and declarations, while preserving their relationships and order. Parsing is the process where the code is analyzed to ensure It follows the grammar rules and syntax defined by the programming language.

In computing, the parser will read the code and convert it into an Abstract Syntax Tree (AST) representation that helps the compiler understand the context of each unit and its relationship to the underlying structure of the source code. Each node in the AST represents a part of the code, and the tree’s structure reflects how the code is organized.

The parsing process has 3 stages:

  1. Lexical Analysis: produces the source code and groups them into lexemes or tokens.

  2. Syntactic Analysis: a validation step to check whether the generated tokens form a valid or meaningful expression.

  3. Semantic Parsing: It uses parse tree (or syntax tree) and symbol look-up tables to determine whether the generated tokens are semantically consistent with a specific programming language such as type checking, control flow checking, and label checking.

How does the parser work?

Parsing can be processed in either a top-down or bottom-up manner:

  • Top-Down Parsing: It begins from the start of the symbol or root of the tree where the trees are built from root to leaves. This might be known as predictive parsing or recursive parsing.

  • Bottom-Up Parsing: It begins from the leaves to the root and ends with the start symbol. This might be known as shift-reduce parsing.

Top-down and bottom-up parsing

Interpreter

💡
An interpreter is a computer program that is used to directly process the intermediate representation of the code (often known as bytecode or bytecode-like instructions), then execute these instructions (commands) directly line by line or statement by statement to produce the output.

Not as the compiler scans the whole program and translates it into the machine code, which is executed directly from the operating system, the interpreter translates code in one statement at a time, which leads to the output from the interpreter running slower than the compiled output from the compiler.

Interpreter Process

Just-In-Time (JIT) Compiler

💡
The compiler is software that converts a high-level language code (source code) to binary code that machines can understand. It can be used to perform several tasks such as error detection and prevention, flow control, syntax analysis, type checking, and optimization. Besides that, It might help the generated machine code be correct and optimized for the hardware being used.

How does the compiler work?

There are several different types of compilers such as Cross compilers, Source-to-Source compilers, Just-in-time (JIT) compilers, Bytecode compilers, Hardware compilers, etc. But in this article, we only focus on the JIT compiler.

Just-in-time (JIT) compiler is a run-time compiler that is used to translate the intermediate representation of code (bytecode) into optimized machine code.

JIT compilation helps JavaScript run faster by optimizing the code while it’s running, leading to quicker start-up and continuous performance improvement. It allows you to reduce the size of the program by eliminating redundant code which helps your program be much smaller and more efficient. Besides that, the JIT compiler can benefit us by optimizing the code for different devices, reducing memory usage, improving performance, and increasing reliability.

Inside of JIT compiler for JS

Call Stack & Memory Heap

  • Call Stack*:* A call stack is where our code is actually executed. It’s responsible for keeping the flow of execution for the application.

  • Memory Heap*:* A heap is an unstructured memory that stores the application data such as variables and objects that our application needs. This is where memory allocation happens

  • Every new function execution context is pushed onto the Call Stack, and then popped off when that function is done and returns the result. When executing the call stack, variables like objects and function definitions are stored in the Memory Heap.

  • Note: variables on the contrary (primitive types, which hold references to objects or values), are not directly stored in the memory heap. Instead, they are stored in the execution context (EC)*, along with other relevant information, such as function arguments and internal function variables.*

Memory Heap & Call Stack In JS

Execution Context (EC)

💡
The Execution Context is the concept that represents the environment that contains the code that's currently running, and everything that aids in its execution.

During the EC run-time, the specific code gets parsed by a parser, the variables and functions are stored in memory, executable byte-code gets generated, and the code gets executed.

There are two main types of Execution Contexts: Global Execution Context & Function Execution Context.

Global Execution Context

💡
It’s the base or default EC that represents the environment in which the global code runs, It means the JS code that is not inside of a function. It’s the first EC to be initialized and created when the script starts running. For every JS file, there can only be one GEC.

In this stage:

  • JS creates a global object (such as window in the browser, globalThis in Node.js)

  • Memory space for variables and functions is allocated in the Global EC.

  • Variables declared var keyword outside of any function become properties of the global object and get assigned the default value of undefined. The let and const are added in the Global Execution Context where they remain in the scope where they were declared and do not receive a default value of.

  • Functions declared in the global scope become global functions that can be accessed throughout the code and are fully stored in memory.

Function Execution Context

💡
Whenever a function is called, the JS engine will create an EC known as FEC within the GEC to evaluate and execute the code within that function. When a function is invoked, a new execution context is created specifically for that function call which means every function call gets its own FEC and therefore there can be more than one FEC in the runtime of a script. This allows each function to have its own set of local variables and a separate scope.

In this stage:

  • The JS engine will create an arguments object that provides access to the arguments passed to the function as if they were elements of an array.

Arrow functions do not have their own “arguments” object. Instead, they inherit the arguments object from their surrounding (parent) regular function.

  • Memory space for variables and functions is allocated in the Function EC.

  • The function has its own scope, which is determined by the scope chain, allowing it to access variables from its outer (lexical) scope.

Whenever there’s a function defined inside another function (a nested function), even if the parent function’s (EC) is removed from the Call Stack, the inner function will still retain access to the variable environment (and scope chain) of its parent function’s EC.

This ability of the inner function to remember and access variables from its lexical scope even after the parent function has completed is what’s called a “closure”.

How are Execution Contexts Created?

The creation of an Execution Context (GEC or FEC) happens in two phases:

  • Creation Phase

  • Execution Phase

Create Phase

During this phase, the EC is first associated with an Execution Context Object (ECO) which stores many important data that is being used by the code itself during its runtime. Before the code is executed, the interpreter will perform 3 main tasks:

  1. Creation of the Variable Object (VO): The engine will create the VO, which is an internal data structure or an object-like container that stores all the variables and function declarations defined within that EC or function’s scope.

  2. Creation of the Scope Chain: After the creation of VO, the engine also sets up the scope chain, which is a chain of Variable Objects that represents the scope hierarchy for the current function. It allows the function to access variables and functions from its scope as well as from its outer or parent scopes (lexical scoping).

  3. Setting the value of the this keyword: The JavaScript this keyword refers to the scope where an Execution Context belongs. Once the scope chain is created, the value of this is initialized by the JS engine.

Execution Phase

During this phase, the actual code will be executed. In this stage, the Variable Object (VO) stored variables with the default value of undefined, hence, the JS engine will read the code in the current EC once again, and then assign or update the actual value of those variables. After that, the code will be parsed by the parser, compiled to the byte-code, and finally executed.

Garbage Collectors

💡
A Garbage Collector is a memory management mechanism that works as a memory monitor that’s responsible for identifying and freeing up unused memory. It looks for unused variables or objects that are referenced by the code or any part of the program and marks them as eligible for GC to prevent memory leaks.

Memory Life Cycle

The memory life cycle has 3 major steps:

  • Allocate the memory

  • Use the allocated memory either to read or write or both

  • Release the allocated memory when it’s no longer used or required anymore

How Does GC Work?

The main concept of the algorithms designed for garbage collection is the concept of reference. An object can have a reference to another object if the previous object has access to the latter.

There are 2 algorithms being used by the GC mechanism in JavaScript:

  1. Reference Counting Algorithm: This algorithm will scan the memory to determine the usefulness of an object by finding out if an object has any other objects referring to it. If an object with zero references (which means any other object does not reference it) will be considered to be garbage or collectible and it’s taken as a garbage value and collected.

  2. Mark and Sweep Algorithm: This algorithm implements the GC mechanism which modifies the problem statement from the “object being no longer needed” to the object being “unreachable”. In JavaScript, a root is a global object. The garbage collector starts from these roots and finds all the objects that are referenced from these roots, then all objects referenced from these, etc (DFS algorithm).

    • In the mark stage, It starts from the root and will find all the objects and mark them as reachable or unreachable

    • In the sweep stage, It will check if the object is marked as unreachable, then It will be taken as garbage and collected. After that, the heap memory will be released. This algorithm defines an unreachable object if it has no references (zero references).

Overview of How Garbage Collectors Work

Web APIs

There are a large number of APIs available in the modern browsers. Web APIs (aka Browser APIs) are part of the browser, which provides us with built-in functionalities that we can access through the JavaScript engine to help us handle complex operations and access the data.

There are some common categories of Web APIs that we might know:

  • Document Object Model (DOM), which is an object representation of the web page that allows developers to manipulate HTML and CSS to make the web page’s UI as we want.

  • Web Storage API, which provides more persistent ways of storing data than the Memory Heap of JS Engine, and it can be accessed through the Window API.

  • Web Workers API, which helps us to handle asynchronous callbacks behind the scenes. It runs our script operation in a background thread separate from the main thread of the application, which prevents the main thread from being blocked or slowed down.

Whenever the JS engine encounters an asynchronous callback, it sends this callback to be processed by the Web API environment.

Queues

💡
Queue is defined as a linear data structure that is open at both ends and the operations are performed in First In First Out (FIFO) order.

In the JavaScript environment, It uses a queue which is called a Callback Queue which stores a list of messages that are waiting to be processed.

Callback Queue stores the callback functions that are sent from the Web APIs in the order in which they were added.

There are 2 types of tasks in the Callback Queue: Micro-tasks and Macro-tasks. Micro-tasks will be executed when the current task (or operation) ends and the micro-task queue is cleared. After all the micro-tasks are done and the queue is cleared, then the macro-task cycle will be started.

Queues

Event Loop

💡
The event loop is a design pattern that waits for and dispatches events in a system. It’s an endless loop, where the JavaScript engine waits for tasks, executes them, and then sleeps and waits for more tasks.

It constantly monitors the state of the call stack and the callback queue. There is a min function which is used to check whether the call stack is empty or not. If the stack is empty, it will look for a new callback from the callback queue, if the callback queue has any tasks, the event loop will pick the task from the queue, put it into the stack, and start the execution process.

The Callback queue has 2 types of tasks: Micro-tasks and Macro-tasks.

How Does Event Loop Work?

Micro-tasks

💡
A micro-task is a function that is executed after the function or program is created. It’s used for scheduling things that are required to be completed immediately after the execution of the current iteration. A micro-task can be used to enqueue other micro-tasks and all micro-tasks are completed before any other macro-tasks take place.

A micro-task queue has a higher priority because the event loop will not move to the next task without the micro-task queue being empty. It means all the tasks inside the micro-tasks queue have to be finished first, then the event loop will look for other tasks (macro-tasks, etc.)

The primary reason that the even loop will execute the micro-tasks is because micro-tasks include mutation observer and promise callbacks which are expected to execute in the most immediate future, It helps us to do stuff asynchronously in a synchronous way and makes sure that any given JS is not under mid-execution.

Examples of micro-tasks: process.nextTick, Promises, queueMicrotask, MutationObserver.

Overview of Micro-task

Macro-tasks

💡
A macro-task is a function that runs after the Call Stack and Micro-task have been cleared. It represents a discrete and independent piece of task. A micro-task is put into a micro-task queue, and It can only be executed when the micro-task queue is empty.

Macro-task is commonly considered the same as the task queue, but the only difference between the macro-task queue and task queue is that the task queue is used for synchronous statements whereas the macro-task queue is used for asynchronous statements.

In comparison, the macro-task queue has a lower priority than the micro-task queue. Macro tasks include parsing HTML, generating DOM, executing main thread JavaScript code, and other events such as page loading, input, network events, timer events, etc.

Examples of macro-tasks: setTimeout(), setInterval(), setImmediate(), requestAnimationFrame, I/O, UI Rendering.

Overview of Macro-task

How Do JS Engine and Runtime Work Together?

In summary, the first thing that happens before code execution is for JavaScript to understand the code by analyzing the code against the grammar of JavaScript language and determining the format of a sentence.

After understanding the code, The JS engine needs to do some other tasks to execute the code such as resolving functions, assigning values of parameters, managing returns, ordering function calls, collecting garbage memories, and preparing machine instructions.

After the JS Engine has finished its job, the Runtime will check the callback queue, and the event loop to grab whatever tasks are inside it to schedule it for execution.

How Do JS Engine and Runtime Work Together?

Summary

  • A JavaScript engine reads, translates, and executes code and is embedded in JavaScript runtime environments (Web Browsers, Node.js, Deno, Bun, etc.)

  • The JavaScript Engine works with a Parser, an Abstract Syntax Tree (AST), an Interpreter, a Profiler, a Compiler, and optimized code, as well as a Call Stack and Memory Heap, to process code quickly and efficiently.

  • The Profiler watches the code and makes optimizations, improving the performance of the JavaScript Engine.

  • A runtime is a program that extends the JavaScript engine and provides additional functionalities, such as relevant APIs to interact with the outside world, to build JavaScript-based software.

  • The JavaScript Runtime consists of Web APIs, the Callback queue, and the Event loop, which together enable JavaScript to run asynchronously by scheduling callbacks from the Web APIs to be added to the stack.

  • The Event Loop monitors the Callstack and Callback Queue and allows asynchronous execution of callback functions by continuously pushing them onto the Callstack.

  • The JavaScript Engine analyzes and prepares the code for execution, while the Runtime executes the code and manages the queues that are associated with code execution.

  • The JavaScript Engine is responsible for syntactic analysis of the source code and creating a machine-understandable code, while the Runtime is used for executing the code.

References

JS Visualizer 9000

AlgoDaily - Daily coding interview questions. Full programming interview prep course and software career coaching.

Understanding the JavaScript Runtime Environment and DOM Nodes | Vahid Dejwakh

In depth: Microtasks and the JavaScript runtime environment - Web APIs | MDN

Javascript Runtime: JS Engine, Event Loop, Call Stack, Execution Contexts, Heap, and Queues

Understanding the JavaScript runtime environment

Event loop: microtasks and macrotasks

What are the microtask and macrotask within an event loop in JavaScript ? - GeeksforGeeks

Inside JavaScript Engines, Part 1: Parsing

Inside JavaScript Engines, Part 2: code generation and basic optimizations

Parser

The JIT compiler

JavaScript Execution Context – How JS Works Behind The Scenes

Garbage collection

Memory management - JavaScript | MDN

Garbage Collection in JavaScript - GeeksforGeeks

Garbage Collection in JavaScript

A Deep Dive Into Garbage Collection in JavaScript

How garbage collection works in JavaScript

Garbage Collection in JavaScript