console.log("hello world");
The above piece of code helps print the text in the console. But how does this work behind the scenes? How does the machine understand this code and execute the desired output?
In this article, we are going to explore the in-depth details of how the JavaScript engine works and how code execution occurs.
Let us assume that we have a source code file. The following steps occur to properly execute the source code file:
STEP 01: Parsing
Parsing is the process of breaking down code into smaller pieces (called tokens) and analyzing its structure to make it understandable for the computer. It checks if the code follows the correct syntax and creates a structured representation of the code.
STEP 02: Abstract Syntax Tree (AST)
An Abstract Syntax Tree (AST) is a tree-like structure created during parsing that represents the code's syntax and logic. Each part of the code (like variables, functions, or operators) becomes a node in the tree, showing how the code is organized and how different parts relate to each other.
Example for Parsing and AST Formation
1. Source Code
let x = 10;
x += 5;
console.log(x);
2. After Parsing
The code is broken into tokens (small meaningful pieces):
let
→ Keywordx
→ Identifier (variable name)=
→ Operator10
→ Literal (value);
→ Punctuationx
→ Identifier+=
→ Operator5
→ Literal;
→ Punctuationconsole
→ Identifier.
→ Punctuationlog
→ Identifier (method name)(
→ Punctuationx
→ Identifier)
→ Punctuation;
→ Punctuation
3. After AST Formation
Program (Entire code: let x = 10; x += 5; console.log(x);)
├── VariableDeclaration (let x = 10;)
│ ├── VariableDeclarator (x = 10)
│ ├── Identifier (x)
│ └── Literal (10)
├── AssignmentExpression (x += 5)
│ ├── Identifier (x)
│ └── Literal (5)
└── ExpressionStatement (console.log(x);)
└── CallExpression (console.log(x))
├── MemberExpression (console.log)
│ ├── Identifier (console)
│ └── Identifier (log)
└── Identifier (x)
STEP 03: Bytecode Generation
The AST is passed to the JIT (Just-In-Time) Compiler.
The JIT Compiler takes the AST and converts it into bytecode.
Bytecode is a lower-level, optimized representation of the code but is not directly executable by the CPU.
Bytecode is designed to be interpreted or executed efficiently by the JavaScript engine's virtual machine.
So this bytecode is directly executed by the bytecode interpreter of the V8 engine
STEP 04: Machine Code Execution
The JavaScript engine uses the Interpreter and JIT Compiler to execute the code:
Initially, the Interpreter starts executing the bytecode directly. This is unoptimised
As the code runs, the JIT Compiler identifies frequently executed parts (hot code) and compiles them into machine code for faster execution.
- Machine code is directly executed by the CPU bypassing the interpreter.
STEP 05: Optimization
Optimization occurs dynamically during code execution. The JavaScript engine applies several techniques to make the code run faster. This involves:
Profiling Hot Code:
- The JIT Compiler monitors the execution and identifies frequently executed code paths, called hot code (e.g., loops or frequently called functions).
Optimized Machine Code:
- The JIT Compiler generates optimized machine code for hot code, assuming specific patterns, such as fixed data types.
De-optimization:
- If assumptions about the code change (e.g., a variable changes from a number to a string), the engine reverts to slower, more generic code to ensure correctness.
Optimization Example
Example Code:
function add(a, b) {
return a + b;
}
for (let i = 0; i < 1000; i++) {
add(10, 20); // Hot code
}
What Happens:
The
add
function is interpreted initially as bytecode.The loop makes the
add
function "hot code" because it is called repeatedly.The JIT Compiler optimizes
add
to handle numeric arguments efficiently by generating machine code.If
add
is later called with non-numeric arguments (e.g.,add('10', '20')
), the engine de-optimizes and falls back to the interpreter.
Once the AST (Abstract Syntax Tree) is formed, it is not directly executed. Here's what happens next:
AST → Bytecode:
- The AST is passed to the JIT (Just-In-Time) Compiler, which converts it into bytecode. This bytecode is a lower-level representation designed to be more efficient for the JavaScript engine to interpret.
Execution of Bytecode:
- The Interpreter executes the bytecode initially. This allows the code to run quickly without waiting for full optimization.
Hot Code Optimization:
- As the code runs, the JIT Compiler monitors "hot code" (frequently executed code paths) and compiles it into machine code for faster execution.
So, the AST is a crucial intermediate step, but it is not run by the Interpreter or JIT directly. Instead, it helps generate the bytecode, which is then interpreted or compiled further based on the situation.
STEP 06 : Memory Management
The Garbage Collector automatically manages memory by removing objects that are no longer reachable.
Common strategies include mark-and-sweep, where unreachable objects are marked and cleaned up.
Key Components in a Modern JavaScript Engine (e.g., V8)
Parser: Converts source code into an AST.
JIT Compiler: Converts AST to bytecode , then identifies hot code and optimises it into machine code for better performance.
Interpreter: Executes bytecode generated by JIT compiler directly.
Garbage Collector: Manages memory during execution.
Flow Overview
Parsing: Converts source code → Tokens → AST.
Compilation: AST → Bytecode.
Execution:
Bytecode is interpreted initially by V8 engine's bytecode interpreter.
Hot code is compiled into optimised machine code.
Machine code runs directly on the CPU.
Optimisation: Frequently executed code paths are optimized dynamically during execution.
Memory Management: Ensures efficient use of memory through automatic garbage collection.
Summary
Parsing converts the code into smaller units (tokens).
AST organizes these tokens into a hierarchical structure that represents how the code works.
The JIT Compiler generates optimized bytecode and machine code for efficient execution.
Optimization ensures the code runs faster by dynamically analyzing and improving performance during runtime.
Memory Management handle memory cleanup efficiently, making JavaScript performant
Conclusion
Understanding how JavaScript executes your code, from parsing to optimization, is like uncovering the magic behind the scenes. Concepts like the AST, bytecode, and the JIT compiler may seem complex at first, but they form the foundation of how modern JavaScript engines, like V8, make your code run efficiently.
Mastering these concepts not only deepens your understanding of JavaScript but also helps you write better, more performant code. Whether you're debugging, optimizing, or simply curious about how JavaScript works under the hood, knowing these inner workings will empower you in your development journey.
But this is just the beginning! In the next article, we’ll dive deeper into another critical part of JavaScript's functionality that will take your skills to the next level.
Stay tuned for more insights and continue exploring the fascinating world of JavaScript! 🚀✨