The Inner Workings of JavaScript: From Code to Execution

The Inner Workings of JavaScript: From Code to Execution

·

6 min read

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):

  1. let → Keyword

  2. x → Identifier (variable name)

  3. = → Operator

  4. 10 → Literal (value)

  5. ; → Punctuation

  6. x → Identifier

  7. += → Operator

  8. 5 → Literal

  9. ; → Punctuation

  10. console → Identifier

  11. . → Punctuation

  12. log → Identifier (method name)

  13. ( → Punctuation

  14. x → Identifier

  15. ) → Punctuation

  16. ; → 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:

    1. Initially, the Interpreter starts executing the bytecode directly. This is unoptimised

    2. 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:

  1. 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).
  2. Optimized Machine Code:

    • The JIT Compiler generates optimized machine code for hot code, assuming specific patterns, such as fixed data types.
  3. 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:

  1. The add function is interpreted initially as bytecode.

  2. The loop makes the add function "hot code" because it is called repeatedly.

  3. The JIT Compiler optimizes add to handle numeric arguments efficiently by generating machine code.

  4. 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:

  1. 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.
  2. Execution of Bytecode:

    • The Interpreter executes the bytecode initially. This allows the code to run quickly without waiting for full optimization.
  3. 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)

  1. Parser: Converts source code into an AST.

  2. JIT Compiler: Converts AST to bytecode , then identifies hot code and optimises it into machine code for better performance.

  3. Interpreter: Executes bytecode generated by JIT compiler directly.

  4. Garbage Collector: Manages memory during execution.


Flow Overview

  1. Parsing: Converts source code → Tokens → AST.

  2. Compilation: AST → Bytecode.

  3. 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.

  4. Optimisation: Frequently executed code paths are optimized dynamically during execution.

  5. 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! 🚀✨