Ever wondered what exactly happens when you run console.log("Hello, World!")?
JavaScript doesn't just magically run in your browser. It goes through some phases inside the JavaScript Engine (like Google's V8) before your CPU can understand it.
Let's break down this journey from raw text to execution: 👀
1. Parsing
Parsing is the process of transforming your raw JavaScript source code into a structured representation that the engine can understand.
The 3 Steps of Parsing:
- Lexical Analysis (Tokenization / Scanning) The source code is broken down into small, meaningful chunks called tokens (keywords, identifiers, literals, operators, etc.). Example:
let x = 10;
Tokens generated: let (keyword), x (identifier), = (operator), 10 (literal), ; (punctuation).
-
Syntax Analysis (Building the AST)
These tokens are then transformed into an Abstract Syntax Tree (AST). While building this tree, the engine discovers declarations (
function,var,let,const). This is exactly where Hoisting and Automatic Semicolon Insertion (ASI) happen. Example:
VariableDeclaration
├─ Identifier: x
└─ Literal: 10
-
Semantic Analysis & Scope Building
The engine checks if the code follows language rules. It builds scopes and catches early syntax errors (e.g., declaring the same
letvariable twice in the same scope) before the code even runs.
2. Compiling
Modern JavaScript engines don't just interpret code, nor do they compile it all ahead of time (like C++). They use Just-In-Time Compilation (JIT) that means compilation happens at runtime.
Here is how V8's JIT architecture handles this:
- The Interpreter (Ignition): Takes the AST, translates it into Bytecode, and executes it immediately. This allows your app to start running extremely fast without waiting for a compiler.
- Profiling: While the Bytecode runs, a background Profiler watches the code and detects hot code paths (functions or loops executed repeatedly).
- The Optimizing Compiler (TurboFan): Takes these "hot" paths and compiles them into highly optimized Machine Code.
3. Executing
Now, the Interpreter (Ignition) executes the unoptimized Bytecode, while the CPU directly executes the fast, optimized Machine Code. However, if the engine's assumptions turn out to be wrong later (e.g., a function that always took a string suddenly takes a number), it performs De-optimization and throws the code back to Ignition's Bytecode.
Important Notes
- Bytecode always stays in memory. The engine never removes it after generating machine code. It acts as the ultimate source of truth because on de-optimization, execution simply jumps back from the invalid machine code to the existing bytecode.
Why keep bytecode around?
- Assumptions can break at any time. A safe fallback is mandatory.
- Bytecode is easier for debugging and garbage collection.
- Re-parsing JavaScript text into bytecode over and over would be incredibly expensive for the CPU.
The Simplified Execution Map
JavaScript Source
↓ (Parsing)
AST
↓ (Ignition)
Bytecode ───────────────> Executed by Interpreter
↓ (Profiling Hot Code)
↓ (TurboFan JIT)
Optimized Machine Code ─────> Faster execution by CPU
↓ (Assumptions break)
De-Optimization
↓
Back to Bytecode (which is still in memory)
Top comments (0)