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

A Comprehensive Explanation of Fundamental Concepts in JavaScript Code Execution

Irene Smolchenko
ITNEXT

--

A display of a clean red car engine.
Photo by Sitraka on Unsplash

JavaScript is single-threaded, with a single call stack but also with support for asynchronous operations, which allows it to handle concurrent tasks without blocking the main execution thread.

Let’s break this down slowly. To put it simply —

  • Single-threaded means that JavaScript code is executed sequentially, one command at a time. It has only one main thread of execution.
  • JavaScript maintains a single call stack, which represents the execution context of functions called in the program. Each function call creates an execution context that is pushed onto the call stack.
  • JavaScript supports asynchronous programming through mechanisms like callbacks, promises, async/await, and timers. These mechanisms allow certain tasks to be scheduled and executed in the background.
  • By using asynchronous operations, JavaScript can handle concurrent tasks efficiently. This means that while some tasks are waiting for their asynchronous operations to complete, the main thread can continue executing other tasks without being blocked.

Now, buckle up!

JavaScript Runtime

JavaScript runtime can be visualized as a “container” that includes the following components:

  • JavaScript Engine — responsible for parsing, interpreting, and executing JavaScript code.
  • Two main queues for managing asynchronous operations:
    - Callback Queue (Event Queue) — holds tasks that are ready to be executed asynchronously.
    - Microtask Queue (Promise Jobs Queue) — holds micro-tasks, typically related to promises.
  • Web APIs — provided by the browser, allowing JavaScript to interact with various browser functionalities, such as DOM manipulation, AJAX requests, and timers.

In general, the combination of these components allows JS to handle synchronous and asynchronous tasks, interact with the browser environment, and manage concurrent operations efficiently.

Let’s dive deeper in the next section.

JavaScript Engine

A JavaScript engine is a program that is responsible for interpreting (processing / reading) and executing JavaScript programs.

Popular JavaScript engines include V8 (used in Google Chrome), SpiderMonkey (used in Firefox), JavaScriptCore (used in Safari), and Chakra (used in older versions of Microsoft Edge 🥀).
Each engine may have its own unique optimizations and performance characteristics, but they all aim to provide efficient and speedy execution of code.

The main components of a typical JavaScript engine are:

  • Parser — a computer program responsible for reading the code and converting it into an abstract syntax tree AST(*) representation. During parsing, the code is analyzed to ensure it follows the grammar rules and syntax defined by the language.
  • Interpreter — after the parser creates the AST, the interpreter (also a computer program) processes the intermediate representation of the code, often referred to as “bytecode” or “bytecode-like instructions.” It directly executes these instructions line by line to produce the output.
  • Compiler — In some cases, modern JavaScript engines use a JIT(**) compiler to translate the intermediate representation (bytecode) into optimized machine code for better performance.
  • Memory Heap — an unstructured memory pool that dynamically allocates and manages memory for objects and functions during the program’s execution. It plays a crucial role in managing memory efficiently and ensuring smooth execution of JavaScript programs.

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(***), along with other relevant information, such as function arguments and internal function variables.

  • Call Stack — a LIFO data structure that keeps track of the execution context of the currently running functions.
    When the code starts running, the global execution context is created and enters the call stack first. Each time a function is called, a new frame is pushed onto the call stack, and when the function completes its execution, the frame is popped off the stack.

    This process continues until the call stack is empty (all functions have finished executing). The global execution context, is the last one to be removed from the call stack.
  • Garbage Collector — a mechanism that’s responsible to identify and free up memory that is no longer in use. It looks for variables or objects that are no longer referenced by the code or any part of the program and marks them as eligible for garbage collection in order to prevent memory leaks.

    For example, when a function completes its execution, the local variables and references within the function’s context are garbage-collected, unless they are referenced by closures.
A line separator

(*) AST

A hierarchical representation (a tree-like data structure) of the syntactic structure of source code.
The parser breaks down the code into its fundamental elements (units), such as expressions, statements, and declarations, while preserving their relationships and order. Each node in the AST represents a part of the code, and the tree’s structure reflects how the code is organized.

(**) JIT

Just-In-Time compilation helps JS run faster by optimizing the code while it’s running, leading to quicker start-up and continuous performance improvement. Instead of waiting to compile the entire code, the engine quickly starts executing using an initial version called “bytecode.”

It cleverly identifies frequently used parts of the code (hotspots) and optimizes them in the background. The engine can optimize the same code multiple times using different techniques based on code behavior. It also uses “code swapping” to switch to better-optimized versions without stopping the program’s execution, making the code continuously faster.

(***) Execution Context (“EC”)

A concept in JS that represents the environment in which code is executed.
There are two main types of Execution Contexts:

  • Global Execution Context — the outermost execution context that represents the environment in which the global code runs. It is created when the script starts running and is the first EC to be set up.
  • Function Execution Context — created each time a function is called. When a function is invoked, a new execution context is created specifically for that function call. This allows each function to have its own set of local variables and a separate scope.

During the “creation phase” of an execution context, before the actual execution of the code starts, the interpreter performs two main tasks:

  • The engine creates the Variable Object, which is an internal data structure that holds all the variables and function declarations within the function’s scope.
  • 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 own scope as well as from its outer or parent scopes.

Additionally, the ‘this’ keyword’s value is determined, but it’s out of the scope of this post.

In the Global Execution Context:

  • JS creates a global object (window in the browser, globalThis in Node.js)
  • Memory space for variables and functions is allocated in the Global EC.
  • Variables declared with var outside of any function become properties of the global object and get assigned the default value of undefined.
    Variables declared with let and const also exist in the Global Execution Context, but they are not added as properties to the global object. They remain in the scope where they were declared and do not receive a default value of undefined.
  • Functions declared in the global scope become global functions accessible throughout the code. They are fully stored in memory.

In the Function Execution Context:

  • Instead of a global object, JS creates an arguments object. It 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”.

Event Loop

For environments with asynchronous capabilities like web browsers, the JavaScript engine also interfaces with the event loop to handle asynchronous operations.

The “Event Loop” is a concept that explains how asynchronous tasks are handled within the JS runtime environment. It monitors the task queues and the call stack, ensuring that pending asynchronous tasks are executed when the call stack is empty. More on this in the next section.

Before moving on to the next JavaScript runtime component, below is a capture of the key steps and processes involved in the JavaScript code execution we’ve learned so far.

1. The code is broken down into tokens during the lexical analysis phase. 2. The tokens are organized into an Abstract Syntax Tree (AST) during the syntax parsing phase. 3. Execution contexts are created for the global scope and each function call. 4. The Variable Object and Scope Chain are created to manage variable access and scope resolution. 5. The JavaScript engine executes the code line by line, updating variables and performing operations. 6. Function calls are managed in the call stack,

Callback and Microtask Queue

Callback Queue (Event Queue)

The Callback Queue is a mechanism used by the JavaScript runtime to handle asynchronous tasks. It is a FIFO data structure that holds callback functions waiting to be executed.

The Event Loop monitors the Call Stack and the Callback Queue, ensuring that callback functions are executed once the Call Stack is empty. This enables JavaScript to manage and execute asynchronous operations efficiently while maintaining its single-threaded nature.

How?

  • After an asynchronous event occurs (a timer expires or a network request completes), the associated callback function is placed into the Callback Queue.
  • When the Call Stack becomes empty (meaning all synchronous tasks are complete), the Event Loop checks the Callback Queue.
  • If there are any callback functions in the Callback Queue, the Event Loop takes the first one and passes it to the Call Stack for execution.

This process continues, allowing asynchronous tasks to be executed in the order they were added to the Callback Queue.

But just like in real life, someone always gets ahead of line.😉

Micro-task Queue (Promise Jobs Queue)

Another internal queue in JavaScript with a FIFO structure. It has a higher priority than the Callback Queue, meaning that microtasks have precedence over regular callback functions.

The Microtask Queue is specifically designed for handling callbacks related to promises. Promises are a type of asynchronous operation in JavaScript. When a promise is settled (fulfilled or rejected), its associated callbacks (then/catch/finally) are scheduled as microtasks and placed in the Microtask Queue.

The Event Loop processes microtasks right after the current task completes and before it continues to the next task or takes callbacks from the Callback Queue. This priority ensures that microtasks are executed before any other queued callbacks, allowing for more predictable and deterministic handling of promise-related code.

For the sake of avoiding any misunderstandings, microtasks are executed after all synchronous tasks but before other asynchronous tasks.

Web APIs

An API stands for Application Programming Interface, which is a standardized way to interact with a machine or system.

When JavaScript code is executed in a browser environment, the JS engine handles the interpretation and execution of the code. For the tasks that go beyond the core JavaScript language features, and require interactions with the browser environment and external resources, Web APIs come into play.

Web APIs are functionalities provided by the browser environment, accessible through the global window object in a web browser. They are not natively available in JavaScript but are crucial for building interactive and dynamic web applications. These APIs enable developers to perform various tasks, like interacting with the DOM, scheduling tasks with timers, making network requests with fetch, and handling data in JSON format.

How do they connect to the JS runtime environment?

Web APIs handle asynchronous tasks, like event handling, network requests, and timers. When these asynchronous tasks are initiated, the Web APIs start their execution and handle them in the background, outside the regular JavaScript execution flow. After completion of these asynchronous tasks, their corresponding callbacks (such as event listeners or promise callbacks) are placed in the Callback Queue.

Then, as mentioned in the sections above, the Callback Queue holds these callbacks for completed asynchronous tasks, and they are ready to be executed when the JavaScript engine’s call stack becomes empty.

www.scaler.com

Wrap Up

I hope you found this explanation helpful ✨ and that you’re not out of breath, as I am after reading this over and over out loud to myself!

From my experience, this information is crucial for interviews, and knowing these concepts serves as a strong foundation for your career in development.

Stay tuned for future content! If you’re new here, feel free to explore my other articles, and consider following me for future updates and more valuable insights.

Like what you see? Buy me a coffee!

By buying me a virtual croissant on Buy Me a Coffee, you can directly support my creative journey. Your contribution helps me continue creating high-quality content. Thank you for your support! :)

--

--

Writer for

🍴🛌🏻 👩🏻‍💻 🔁 Front End Web Developer | Troubleshooter | In-depth Tech Writer | 🦉📚 Duolingo Streak Master | ⚛️ React | 🗣️🤝 Interviews Prep