From Code to Execution: Exploring JavaScript Function Processing in Modern Browsers

Dev Balaji
5 min readJun 21, 2024

--

JavaScript, a versatile and widely-used programming language, is fundamental to modern web development. It enables dynamic content, interactive features, and seamless user experiences on web pages. However, the execution of JavaScript code involves a complex process that is often hidden from developers. Understanding how a JavaScript function operates, how it is processed by the browser’s JavaScript engine, and the intricate workings behind the scenes can significantly enhance one’s ability to write efficient and optimized code. In this note, we will demystify the internal mechanics of JavaScript by breaking down a simple function example, shedding light on its journey from written code to executed instructions. Let’s explore this fascinating process step by step.

Example JavaScript Function

Let’s consider a simple JavaScript function that takes a number, squares it, and then returns the result:

function square(number) {
return number * number;
}

How the Function is Executed

  1. Parsing the Code:
  • When the browser loads a web page containing JavaScript, it first parses the code. Parsing involves reading the JavaScript code and converting it into an Abstract Syntax Tree (AST). The AST represents the structure of the code in a tree format.

VISUAL REPRESENTATION OF AST:


Program
└── FunctionDeclaration
├── id: Identifier (square)
├── params:
│ └── Identifier (number)
└── body: BlockStatement
└── body: [ ReturnStatement
└── argument: BinaryExpression
├── left: Identifier (number)
└── right: Identifier (number)
]
FunctionDeclaration
- name: "square"
- params: ["number"]
- body:
- ReturnStatement
- BinaryExpression
- left: Identifier "number"
- operator: "*"
- right: Identifier "number"

2. Compilation:

  • Modern JavaScript engines, like V8 (used in Chrome and Node.js) or SpiderMonkey (used in Firefox), use Just-In-Time (JIT) compilation. This means that the JavaScript code is compiled to machine code at runtime, rather than being interpreted line-by-line.
  • The engine takes the AST and generates bytecode, which is a lower-level representation of the code. This bytecode is then optimized by the engine’s JIT compiler into machine code that can be executed by the CPU.
0: LoadParam 0            ; Load the first parameter (number) onto the stack
1: LoadParam 0 ; Load the first parameter (number) onto the stack again
2: Multiply ; Multiply the two top values on the stack
3: Return ; Return the result from the top of the stack


Machine Code (Baseline)
0x1000: Load number
0x1004: Multiply number by number
0x1008: Return result

Profiling Data
- square(2) called 10 times
- square(3) called 15 times
- All arguments are of type "number"

Optimized Machine Code
0x2000: Load number (optimized)
0x2004: Inline multiplication (optimized)
0x2008: Return result (optimized)

Deoptimized Code
0x3000: Load param (generic)
0x3004: Check types
0x3008: Multiply (generic)
0x300C: Return

Detailed Breakdown

Let’s break down the bytecode further with a more detailed explanation:

  1. LoadParam 0:
  • The first parameter (number) is pushed onto the stack.
  • In our function call square(5), the value 5 is loaded.

2. LoadParam 0:

  • The parameter (number) is pushed onto the stack again.
  • This gives us two instances of 5 on the stack.

3. Multiply:

  • The top two values on the stack (5 and 5) are popped off the stack.
  • The multiplication operation is performed: 5 * 5 = 25.
  • The result (25) is pushed back onto the stack.

4. Return:

  • The value on the top of the stack (25) is returned as the result of the function.

Visual Representation

Here’s a visual representation of the stack operations:

Stack Before Operations:
Step 0: LoadParam 0
Stack:
[5]
Step 1: LoadParam 0
Stack:
[5, 5]
Step 2: Multiply
Stack:
[25]
Step 3: Return
Result:
25

3. Execution:

  • Once the code is compiled, the engine begins execution. Here’s how our square function would be executed step-by-step:
  • The function square is called with an argument, say 5.
  • The engine creates an execution context for this function call. An execution context contains information about the function’s environment, including variable values, the scope chain, and this binding.
  • The parameter number is assigned the value 5.
  • The expression number * number is evaluated, resulting in 25.
  • The result 25 is returned to the caller.

Execution Context Diagram:

Global Execution Context
- Variable Environment
- square: Function
- result: undefined
- Lexical Environment
- Scope Chain
- This Binding: Global Object
------------------------------
Function Execution Context (square)
- Variable Environment
- number: 5
- Lexical Environment
- Scope Chain
- [Global Scope]
- This Binding: Global Object

Behind the Scenes

JavaScript Engine Components

  1. Call Stack:
  • The call stack is a data structure that keeps track of function calls. Each time a function is called, a new frame is pushed onto the stack. When a function returns, the frame is popped off the stack.
  • For our square(5) call, a stack frame for square is added to the call stack, and once the function returns, it is removed.

2. Memory Heap:

  • The memory heap is where objects, strings, and closures are stored. In our example, there isn’t much happening in the heap since we are dealing with simple number operations.

3. Execution Context:

  • Each function invocation creates a new execution context. This context holds variables, the scope chain, and the value of this. In our example, the execution context for square(5) contains the parameter number with the value 5.

4. Garbage Collection:

  • JavaScript engines include garbage collectors that free up memory allocated to objects that are no longer needed. In our simple example, the number 5 and the result 25 are stored in stack memory and cleaned up once the function execution is complete.

Summary

  1. Initial Parsing: The function square(number) is parsed into an AST.
  2. Bytecode Generation: The AST is converted into bytecode by the interpreter.
  3. Baseline Compilation: The bytecode is compiled into machine code with basic optimizations.
  4. Profiling: The engine collects runtime profiling data on the function’s execution.
  5. Optimization: The optimizing compiler uses profiling data to generate highly optimized machine code.
  6. Deoptimization: If runtime assumptions are violated, the engine can revert to less optimized code.

Diagram Summary

Below is a simplified diagram to illustrate the process:

JavaScript Code
|
v
Parsing
|
v
AST (Abstract Syntax Tree)
|
v
Bytecode (Interpreter)
|
v
Baseline JIT Compiler
|
v
Machine Code (Baseline)
|
v
Profiling (Runtime Data Collection)
|
v
Optimizing JIT Compiler
|
v
Optimized Machine Code
|
v
Deoptimization (if needed)

Further Reading

To explore more about JIT compilation and JavaScript engines, you can refer to the following resources:

  1. An Introduction to Just-in-Time (JIT) Compilation
  2. JavaScript Engine Fundamentals: Shapes and Inline Caches
  3. Inside look at modern web browser (part 1)
  4. JIT Compilation in V8

--

--

Dev Balaji
Dev Balaji

Written by Dev Balaji

🚀 Tech Enthusiast | 🌟 Mastering JavaScript & Frameworks | 💡 Sharing Tips & Tricks | 📘 Aspiring Blogger & Architect | 🐍 Python Practitioner

No responses yet