What is a JavaScript Engine?
JavaScript Engine Overview
What is a JavaScript Engine?
A JavaScript engine is a program (written in C++) that:
- Parses your JS source code into a structured tree
- Compiles that tree into machine code
- Executes the machine code on the CPU
- Manages memory (allocates and frees objects)
Every browser has one built in. Node.js, Deno, and Bun are built on top of one.
V8
Built by Google. Used in Chrome, Node.js, Deno, Bun, and Edge. The fastest and most widely deployed engine.
SpiderMonkey
Built by Mozilla. Used in Firefox. The world's first JavaScript engine — created by Brendan Eich in 1995.
JavaScriptCore
Built by Apple. Used in Safari and all iOS browsers. Also called Nitro. Optimized for power efficiency.
Hermes
Built by Meta. Used in React Native. Compiled ahead-of-time for faster startup on mobile devices.
How Code Goes from Text to Execution
How Code Goes from Text to Execution
1. Source Code
Your JavaScript file arrives as raw text — a string of characters. The engine doesn't understand any of it yet.
1function add(a, b) {2 return a + b;3}4console.log(add(2, 3));
2. Lexing / Tokenization
The Lexer (also called Scanner or Tokenizer) reads the source character by character and groups them into tokens — the smallest meaningful units of the language. Whitespace and comments are discarded.
1// Source: function add(a, b) { return a + b; }2// Tokens produced:3[4 { type: 'keyword', value: 'function' },5 { type: 'identifier', value: 'add' },6 { type: 'punctuation', value: '(' },7 { type: 'identifier', value: 'a' },8 { type: 'punctuation', value: ',' },9 { type: 'identifier', value: 'b' },10 { type: 'punctuation', value: ')' },11 { type: 'punctuation', value: '{' },12 { type: 'keyword', value: 'return' },13 { type: 'identifier', value: 'a' },14 { type: 'operator', value: '+' },15 { type: 'identifier', value: 'b' },16 { type: 'punctuation', value: ';' },17 { type: 'punctuation', value: '}' }18]
3. Parsing → AST
The Parser takes the token list and builds an Abstract Syntax Tree (AST) — a tree structure that represents the grammatical structure of your code. This is where syntax errors are caught.
1// AST for: function add(a, b) { return a + b; }2{3 "type": "FunctionDeclaration",4 "id": { "type": "Identifier", "name": "add" },5 "params": [6 { "type": "Identifier", "name": "a" },7 { "type": "Identifier", "name": "b" }8 ],9 "body": {10 "type": "BlockStatement",11 "body": [{12 "type": "ReturnStatement",13 "argument": {14 "type": "BinaryExpression",15 "operator": "+",16 "left": { "type": "Identifier", "name": "a" },17 "right": { "type": "Identifier", "name": "b" }18 }19 }]20 }21}
4. Bytecode Generation (Interpreter)
In modern engines, the AST is first compiled into bytecode — a compact, intermediate representation. The interpreter (V8 calls it Ignition) executes bytecode immediately. It's slower than machine code but starts executing fast without waiting for full compilation.
1// Bytecode (V8 Ignition — simplified illustration)2// add(a, b) { return a + b }3LdaUndefined // Load undefined4Star r0 // Store in register 05Ldar a0 // Load parameter 'a'6Add a1, [0] // Add parameter 'b'7Return // Return result
5. JIT Compilation (Optimizing Compiler)
As the engine runs bytecode, it profiles the code — tracking which functions are called often ("hot"). Hot functions get compiled to native machine code by the optimizing compiler (V8 calls it TurboFan). Machine code runs 10–100× faster than bytecode.
1// After JIT: add(2, 3) compiled to x64 machine code2// (simplified illustration)3mov eax, [arg_a] ; Load 'a' into register eax4add eax, [arg_b] ; Add 'b' to eax5ret ; Return result in eax67// The CPU executes this directly — no interpretation needed
6. Garbage Collection
While executing, the engine continuously tracks memory. Objects no longer reachable get removed by the Garbage Collector (GC) — freeing memory automatically. V8 uses a generational GC called Orinoco.
1function createUser() {2 const user = { name: 'Alice', age: 25 }; // allocated on heap3 return user;4}56let u = createUser(); // 'user' object kept alive (referenced by u)7u = null; // no more references → GC can collect it8// At some point, GC runs and frees the memory
JIT Compilation — The Speed Secret
JIT Compilation — The Speed Secret
Source code↓Read line by line↓Execute each line↓(Repeat for every run)✅ Starts immediately✅ No compile wait❌ Slow — re-interpretsevery line every time❌ Can't optimizeacross iterations
Source code↓Parse → AST → Bytecode↓Interpret + Profile↓'Hot' code detected?↓Compile to machine code↓Run native machine code✅ Fast startup✅ Gets faster over time✅ Applies type-specificoptimizations✅ Can deoptimize safely
Source code↓Read line by line↓Execute each line↓(Repeat for every run)✅ Starts immediately✅ No compile wait❌ Slow — re-interpretsevery line every time❌ Can't optimizeacross iterations
Source code↓Parse → AST → Bytecode↓Interpret + Profile↓'Hot' code detected?↓Compile to machine code↓Run native machine code✅ Fast startup✅ Gets faster over time✅ Applies type-specificoptimizations✅ Can deoptimize safely
| Tier | V8 Name | Speed | When Used | Trade-off |
|---|---|---|---|---|
| Interpreter | Ignition | Medium | All code on first run | Fast startup, no wait |
| Optimizing Compiler | TurboFan | Very Fast | Hot functions (called many times) | Slow to compile, but much faster to execute |
| Deoptimization | — | Back to Ignition | When type assumptions break | Falls back if JS types change unexpectedly |
Write type-stable code for better JIT performance
JIT compilers make assumptions about types. If a function always receives numbers, V8 compiles a fast number-optimized version. If you then pass a string, V8 must deoptimize (throw away the fast code and fall back to bytecode). This is called a deoptimization bailout and hurts performance.
Write functions that always receive the same types — don't mix numbers and strings in the same variable across calls.
1// ✅ JIT-friendly — consistent types2function addNumbers(a, b) {3 return a + b; // always numbers → JIT compiles fast numeric add4}56for (let i = 0; i < 1000000; i++) {7 addNumbers(i, i + 1); // V8 detects 'hot', compiles to machine code8}9console.log(addNumbers(5, 3)); // 8 — runs as native machine code101112// ❌ JIT-unfriendly — inconsistent types (deoptimizes)13function mixedAdd(a, b) {14 return a + b; // sometimes number+number, sometimes string+number15}1617for (let i = 0; i < 500; i++) {18 mixedAdd(i, i + 1); // V8 assumes: number + number19}20mixedAdd('hello', 5); // DEOPT! — assumption broken, falls back to bytecode21// V8 now treats the function as polymorphic — slower
Memory — Stack and Heap
Memory — Stack and Heap
What it stores:Primitive values(number, string, boolean,null, undefined, symbol)Function call framesLocal variablesReturn addressesHow it works:LIFO — Last In, First OutFixed size per frameAuto-managed (push/pop)Speed: Very fastSize: Limited (~1MB)Error: Stack Overflow(too many nested calls)
What it stores:Objects {}Arrays []FunctionsClosuresEverything non-primitiveHow it works:Unstructured memory poolEngine allocates on demandGarbage Collector freesunused objectsSpeed: Slower (GC overhead)Size: Large (limited by RAM)Error: Out of Memory(too many live objects)
What it stores:Primitive values(number, string, boolean,null, undefined, symbol)Function call framesLocal variablesReturn addressesHow it works:LIFO — Last In, First OutFixed size per frameAuto-managed (push/pop)Speed: Very fastSize: Limited (~1MB)Error: Stack Overflow(too many nested calls)
What it stores:Objects {}Arrays []FunctionsClosuresEverything non-primitiveHow it works:Unstructured memory poolEngine allocates on demandGarbage Collector freesunused objectsSpeed: Slower (GC overhead)Size: Large (limited by RAM)Error: Out of Memory(too many live objects)
1// STACK — primitives stored by value2let x = 10; // x stored on stack3let y = x; // y gets a COPY of x's value4y = 20;5console.log(x); // 10 — x is unchanged6console.log(y); // 20 — independent copy78// HEAP — objects stored by reference9let obj1 = { name: 'Alice' }; // object on heap, obj1 holds reference10let obj2 = obj1; // obj2 copies the REFERENCE, not the object11obj2.name = 'Bob';12console.log(obj1.name); // 'Bob' — same object!13console.log(obj2.name); // 'Bob'1415// CALL STACK — function frames16function outer() {17 let a = 1; // pushed onto stack18 function inner() {19 let b = 2; // pushed onto stack20 return a + b; // reads a from outer frame21 } // inner frame popped off stack22 return inner();23} // outer frame popped off stack2425console.log(outer()); // 3
Stack Overflow
A Stack Overflow happens when the call stack grows too deep — usually from infinite or deeply nested recursion. Each function call adds a frame to the stack. When the stack limit is exceeded (typically ~10,000 frames in V8), you get:
RangeError: Maximum call stack size exceeded
This is where the name of the famous developer Q&A site comes from.
Garbage Collection
Garbage Collection
The Reachability Principle
The GC's core rule: if an object cannot be reached by following references from the root (global scope), it is garbage and can be collected.
Roots include: global variables, the current call stack, and closures. Everything reachable from a root is kept alive. Everything else is freed.
1// ✅ Object becomes unreachable → eligible for GC2function createData() {3 let bigArray = new Array(1000).fill('data'); // allocated on heap4 return bigArray[0]; // only returning first element5 // bigArray itself has no external reference after function returns6 // → GC will collect it7}8console.log(createData()); // 'data'91011// ❌ Memory Leak — event listener never removed12const button = document.createElement('button');13const largeData = new Array(10000).fill('important');1415function handleClick() {16 // This closure captures 'largeData'17 console.log(largeData[0]);18}1920button.addEventListener('click', handleClick);21// If button is removed from DOM but handleClick is never removed,22// largeData stays in memory forever2324// ✅ Fix: always remove listeners you no longer need25// button.removeEventListener('click', handleClick);2627console.log('Reachability demonstrated');
Inside V8 — A Closer Look
Inside V8 — A Closer Look
| V8 Component | Role | Details |
|---|---|---|
| Ignition | Interpreter / Bytecode Generator | Converts AST to bytecode. Executes immediately. Also profiles execution to find hot functions. |
| TurboFan | Optimizing JIT Compiler | Compiles hot bytecode to optimized machine code. Uses type feedback from Ignition. |
| Liftoff | Baseline Compiler (WebAssembly) | Fast first-tier compiler for WebAssembly modules. |
| Orinoco | Garbage Collector | Generational, incremental, concurrent GC. Minimizes pause times. |
| Torque | Internal Language | DSL used to write V8's built-in JS methods (Array.map, etc.) in a type-safe way. |
| Maglev | Mid-tier Compiler (since 2023) | New middle tier between Ignition and TurboFan. Faster than Ignition, cheaper to compile than TurboFan. |
Hidden Classes — V8's Shape Optimization
V8 creates Hidden Classes (also called Shapes or Maps) to optimize property access on objects. When all objects have the same properties in the same order, V8 treats them as the same 'shape' and uses a fast lookup table.
Key rule: Always initialize all object properties in the constructor, in the same order. Avoid adding properties after object creation — it creates a new hidden class and slows down property lookups.
1// ✅ Fast — same shape, V8 uses one hidden class for all2class Point {3 constructor(x, y) {4 this.x = x; // always in same order5 this.y = y;6 }7}8const p1 = new Point(1, 2);9const p2 = new Point(3, 4);10const p3 = new Point(5, 6);11// All share the same hidden class → fast property access121314// ❌ Slow — different shapes, V8 creates multiple hidden classes15const a = {};16a.x = 1; // hidden class A: { x }17a.y = 2; // hidden class B: { x, y }1819const b = {};20b.y = 2; // hidden class C: { y }21b.x = 1; // hidden class D: { y, x } ← different order!22// a and b have DIFFERENT hidden classes even with same properties23// → V8 cannot optimize property access the same way2425console.log(p1.x + p2.y); // 1 + 4 = 5 (fast path)26console.log(a.x + b.x); // 1 + 1 = 2 (slower path)
Engine vs Runtime — What's the Difference?
Engine vs Runtime — What's the Difference?
JavaScript Engine
The core program that parses and executes ECMAScript. Only knows about what's in the spec — no DOM, no timers, no network. Pure language execution.
Browser Runtime
The engine + Web APIs provided by the browser. Adds DOM, fetch, setTimeout, localStorage, WebWorkers, Canvas. The full environment for frontend code.
Node.js Runtime
V8 engine + Node APIs. Adds fs (filesystem), http, crypto, process, Buffer. The full environment for backend/server code.
Deno / Bun Runtime
Alternative runtimes built on V8 (Deno) or a fork of JavaScriptCore (Bun). Each provides its own set of system APIs with different design goals.
The Engine Doesn't Know About the DOM
The JavaScript engine itself — V8, SpiderMonkey, JavaScriptCore — has zero knowledge of the browser, the DOM, or any web APIs. It only knows how to execute ECMAScript.
setTimeout is not in V8. It's in the browser's Web APIs layer. When you call setTimeout, the browser passes it to its own timer system, and when the time is up, it puts the callback into the Event Queue for the engine to pick up.
How the Browser Uses the Engine
How the Browser Uses the Engine
1. HTML Parser hits a <script> tag
The HTML parser encounters <script src="app.js"> or an inline <script> block. Parsing of HTML pauses (unless defer or async is used) while the script is fetched and executed.
2. Script handed to the JS Engine
The browser passes the JavaScript source text to V8 (or the browser's engine). V8 tokenizes, parses, generates bytecode, and begins executing.
3. Engine calls Web API for async work
When your code calls setTimeout(fn, 1000) or fetch(url), the engine hands that request to the Web APIs layer (managed by the browser, not V8). The engine continues executing without waiting.
4. Web API completes, pushes to Callback Queue
When the 1-second timer fires, or the fetch response arrives, the Web API puts the callback function into the Callback Queue (also called the Task Queue).
5. Event Loop picks up the callback
The Event Loop continuously checks: Is the Call Stack empty? Is there anything in the Callback Queue? When the stack is empty, it moves the callback from the queue onto the stack.
6. Engine executes the callback
V8 executes the callback function. This cycle repeats indefinitely — the engine runs code, delegates async work to Web APIs, the Event Loop shuttles callbacks back when ready.
1console.log('1 — Start'); // Runs immediately (synchronous)23setTimeout(() => {4 console.log('3 — setTimeout'); // Runs LAST — macrotask queue5}, 0);67Promise.resolve().then(() => {8 console.log('2 — Promise.then'); // Runs BEFORE setTimeout — microtask queue9});1011console.log('1 — End'); // Runs immediately (synchronous)1213// Output order:14// 1 — Start15// 1 — End16// 2 — Promise.then ← microtask queue (runs before next macrotask)17// 3 — setTimeout ← macrotask queue1819// WHY? The event loop empties the microtask queue (Promises)20// completely before moving to the next macrotask (setTimeout)
Test Your Knowledge
Test Your Knowledge
What is the correct order of stages in a modern JavaScript engine pipeline?
What is JIT compilation?
Where are JavaScript objects and arrays stored in memory?
Which of these is provided by the browser runtime, NOT the ECMAScript engine?
What is a 'deoptimization bailout' in JIT compilation?
Engine Performance Tips
Engine Performance Tips
- 1**Keep types consistent** — functions that always receive the same types get compiled to fast JIT machine code. Mixing types causes deoptimization.
- 2**Initialize all object properties in the constructor** — this gives all instances the same hidden class shape, enabling fast property lookups.
- 3**Avoid adding properties to objects after creation** — each new property creates a new hidden class, slowing down V8's property access optimization.
- 4**Avoid deeply nested recursion** — every call pushes a frame onto the Call Stack. Too deep = RangeError: Maximum call stack size exceeded.
- 5**Remove event listeners when no longer needed** — forgotten listeners are one of the most common sources of memory leaks in JavaScript apps.
- 6**Don't create unnecessary short-lived objects in hot loops** — every object allocation triggers the GC eventually. Reuse objects where performance matters.
- 7**Use `const` by default, `let` when needed, never `var`** — `const` and `let` are block-scoped which helps the engine optimize scope resolution.
- 8**Profile before optimizing** — use Chrome DevTools Performance and Memory tabs to find actual bottlenecks. Don't guess.
Event Loop & Async
Deep dive into the Call Stack, Callback Queue, Microtask Queue and how async JavaScript actually works.
Variables & Data Types
Learn how primitives and objects are stored differently in memory — Stack vs Heap in practice.
Memory Management
Understand closures, garbage collection, and how to avoid memory leaks in real applications.
Performance Optimization
Practical techniques for writing JavaScript that V8 can compile and run as efficiently as possible.
You now understand how JavaScript engines work!
You know that your code goes through tokenization → parsing → bytecode → JIT compilation. You understand why type stability matters, how memory is split between Stack and Heap, and how the GC reclaims unused objects. This knowledge will make you a better JavaScript developer.
Try it in the Javascript Compiler
Run and experiment with Javascript code right in your browser — no setup needed.