3 October 2023

Demystifying JavaScript Engines: From Parsing to Execution

JavaScript

Web

A comprehensive guide to understanding what happens when you feed code to your browser.
Piotr Sławiński
Piotr Sławiński
Frontend Developer
Demystifying JavaScript Engines: From Parsing to Execution

Have you ever wondered what exactly happens when you feed your browser some code? The process between parsing your code and executing it on the CPU is fascinating and often misunderstood. In this blog post, we’ll dive deep into this process to fill in the gaps in your understanding.

Understanding JavaScript Engines

To start, let’s define what a JavaScript engine is. In simple terms, it’s a program responsible for:

  • Translating source code into machine code and executing it on the CPU.
  • Providing mechanisms for parsing and Just-In-Time (JIT) compilation.


If you want to score extra points, you might want to add that:

  • JavaScript engines adhere to the ECMA standard.
  • Multiple JavaScript engines exist, including V8 in Chrome, Spidermonkey in Firefox and JavaScriptCore in Safari.
  • Different JavaScript engines may handle code differently.


Imagine that we’re dropping an HTML file with some markup and a script tag containing the following JavaScript code into our browser:

Code

1function helloWorld() { return "hello world"; }
2

Here’s what happens step by step:

  1. Initially, the HTML parser encounters the <script> tag, but it does not involve the JavaScript engine just yet.
  2. Decoding.
  3. The script is loaded from the network, cache, or service worker and then passed to a byte stream decoder as bytes for each character.
  4. Tokenizing.
  5. The byte stream decoder creates tokens (like function, helloWorld, (, ) etc.) from the decoded stream of bytes
  6. Parsing.
  7. The tokens are then parsed by pre-parser (a.k.a lazy parsing, for code required later) and the parser (a.k.a eager parsing, for code required immediately). Parsed tokens create nodes of the Abstract Syntax Tree (like Program, FunctionLiteral etc.)
  8. Code generation.
  9. The interpreter goes through the AST and generates unoptimized bytecode, executable by the engine’s virtual machine. Once bytecode is generated, the AST is discarded.
  10. Optimization.
  11. If the code, or part of it, is executed multiple times, the JavaScript engine may start translating the code into highly optimized machine code using JIT compilation. This optimized machine code is then executed directly on the CPU.


Understanding Just-In-Time (JIT) Compilation

During the code execution step, a compilation is performed, which differs from Ahead-Of-Time compilation conducted before the code is run, thereby allowing for distinct code optimization tailored to each user or use case. This is one of the reasons JavaScript can be so performant. Here’s what happens:

  1. While executing bytecode, a monitor (profiler) observes how many times the code runs and the types being used. Code that runs frequently is considered “warm,” and if it runs very often, it’s “hot.”
  2. When a function becomes “warm,” the JIT compiler sends it to the baseline compiler (e.g., Ignition in V8) to compile it into machine code, which is stored for future execution.
  3. If a portion of the code becomes “hot,” the monitor sends it to the optimizing compiler (e.g., TurboFan in V8), creating an even faster version of the function, also stored for execution.
  4. If the code unexpectedly returns different data types or changes object structures (a.k.a. “shape”), the machine code is de-optimized, and the engine reverts to interpreting the original bytecode.


In the end, all code must eventually run as machine code on the CPU. The key difference between interpreted machine code and optimized machine code is that the interpreted code runs without control from the engine, whereas optimized machine code is meticulously examined to execute only the necessary CPU instructions.


Regardless, whenever code is executed in JavaScript, it operates within an execution context, and the JavaScript engine creates and manages these contexts on a call stack.

Understanding Execution Contexts

In simplest words, an execution context is a place in the memory where the JavaScript code is evaluated and executed (or called/invoked - it means the same thing). Execution contexts are fundamental to JavaScript’s execution model. Here’s a breakdown:

  • When a javascript engine (during translation or optimization) goes through the code line-by-line in a single-threaded manner, it creates execution contexts.
  • The abstract concept which goes line-by-line and in-and-out of functions is called the thread of execution.
  • The first time a script is executed, a global execution context is created, even if the script has no code.
  • When a function is called, a new functional execution context is created.
  • Special execution contexts exist for functions like eval().
  • Each execution context is being added to the call stack during its creation phase.
  • Execution context gets popped off from the call stack at the end of its execution phase.


The Global Execution Context

  • The global execution context encompasses code outside of any function, making everything outside a function part of it.
  • There can only be one global execution context in a program.


Functional execution context

  • A new functional execution context is created every time a function is called.
  • The number of functional execution contexts corresponds to the number of function calls in the code.


Each execution context has two phases: the creation phase and the execution phase.


Creation phase

During the creation phase, the following environments are established:

  • Lexical environment consisting of:
  • An Environment record for variable and function declarations within the lexical environment. It includes:
  • A Declarative environment record for functions, also storing the arguments object.
  • An Object environment record for code outside functions (global scope), also storing the global binding object.
  • A Reference to the outer Lexical environment, which the JavaScript engine uses to look up variables not found in the current Lexical environment (a possibly null reference).
  • The this binding.
  • Variable environment also a Lexical environment but specifically for variable bindings (e.g., var).


In summary, during this phase:

Global execution context Functional execution context

A global object (window for a browser or global for node) representing the executing code’s context is created in the Object environment record.

An arguments object containing references to the function’s parameters is created in the Declarative environment record.

Memory in Object environment record is allocated for the variables and function declaration within the global execution context (defined globally).

Memory in Declarative environment record is allocated for the variables and function declaration within the function execution context (defined within the function).

The this variable referring to the global object is created.

The this variable is created, referring to the global object or to an object to which the current code that’s being executed belongs.

Variables are initialized as undefined (except let and const which will remain uninitialized). Functions are directly placed in memory. This explains why accessing a var variable before assigning a value results in undefined, while doing the same with let and const leads to a ReferenceError.


It is when hoisting takes place as well as where closures are created.


Execution phase

The first function to be executed is the one with its execution context at the top of the call stack.


Execution context gets popped off the stack when assignments to all the variables are done, and the code is executed (function is returned).


Finally, when the global execution context gets popped off the call stack, the program ends.


If a function is asynchronous or uses objects not provided by the JavaScript engine, the Web API, queue and event loop steps in, but that’s a story for another article so stay tuned!

Conclusions

Well, this is it. That is how JavaScript engines work at a high to medium level. While each concept could be explored in even greater detail, this should give you a solid foundation. Keep on coding and exploring the fascinating world of JavaScript!

blazity comet

Get a quote

Empower your vision with us today
Brian Grafola, CTO at Vibes

“We are very happy with the outcomes and look forward to continuing to work with them on larger initiatives.”

Brian Grafola

CTO at Vibes

Trusted by
Solana logoVibes logoArthur logoHygraph logo
The contact information is only used to process your request. By clicking send, you agree to allow us to store information in order to process your request.