javascript

JavaScript Engine Overview

Learn how JavaScript engines work — parsing, compilation, JIT optimization, memory management, and how code goes from text to execution inside V8, SpiderMonkey, and JavaScriptCore.

17 min read 10 sections Tutorial
Share

What is a JavaScript Engine?

JavaScript Engine Overview

Every time you open a webpage or run a Node.js script, a **JavaScript engine** reads your code, understands it, and executes it. The engine is the core piece of software that makes JavaScript run. Without a JS engine, your code is just text — a plain string with no meaning. The engine gives it life by converting your human-readable JavaScript into machine instructions that the CPU can actually execute.

What is a JavaScript Engine?

A JavaScript engine is a program (written in C++) that:

  1. Parses your JS source code into a structured tree
  2. Compiles that tree into machine code
  3. Executes the machine code on the CPU
  4. 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

When the engine receives your JavaScript source code, it goes through a multi-stage pipeline before any instruction runs on your CPU. Each stage transforms your code into something closer to what the machine understands.
1

1. Source Code

Your JavaScript file arrives as raw text — a string of characters. The engine doesn't understand any of it yet.

javascript
1function add(a, b) {
2 return a + b;
3}
4console.log(add(2, 3));
2

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.

javascript
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

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.

javascript
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

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.

text
1// Bytecode (V8 Ignition — simplified illustration)
2// add(a, b) { return a + b }
3LdaUndefined // Load undefined
4Star r0 // Store in register 0
5Ldar a0 // Load parameter 'a'
6Add a1, [0] // Add parameter 'b'
7Return // Return result
5

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.

text
1// After JIT: add(2, 3) compiled to x64 machine code
2// (simplified illustration)
3mov eax, [arg_a] ; Load 'a' into register eax
4add eax, [arg_b] ; Add 'b' to eax
5ret ; Return result in eax
6
7// The CPU executes this directly — no interpretation needed
6

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.

javascript
1function createUser() {
2 const user = { name: 'Alice', age: 25 }; // allocated on heap
3 return user;
4}
5
6let u = createUser(); // 'user' object kept alive (referenced by u)
7u = null; // no more references → GC can collect it
8// At some point, GC runs and frees the memory

JIT Compilation — The Speed Secret

JIT Compilation — The Speed Secret

JavaScript was originally a slow, purely interpreted language. Modern engines are fast because of **Just-In-Time (JIT) compilation** — a technique that compiles code to native machine instructions *while the program is running*, rather than before.
Traditional Interpreter
Source code
Read line by line
Execute each line
(Repeat for every run)
✅ Starts immediately
✅ No compile wait
❌ Slow — re-interprets
every line every time
❌ Can't optimize
across iterations
VS
JIT Compiler (Modern JS)
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-specific
optimizations
✅ Can deoptimize safely
V8's JIT pipeline specifically works in two tiers:
TierV8 NameSpeedWhen UsedTrade-off
InterpreterIgnitionMediumAll code on first runFast startup, no wait
Optimizing CompilerTurboFanVery FastHot functions (called many times)Slow to compile, but much faster to execute
DeoptimizationBack to IgnitionWhen type assumptions breakFalls 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.

javascript
1// ✅ JIT-friendly — consistent types
2function addNumbers(a, b) {
3 return a + b; // always numbers → JIT compiles fast numeric add
4}
5
6for (let i = 0; i < 1000000; i++) {
7 addNumbers(i, i + 1); // V8 detects 'hot', compiles to machine code
8}
9console.log(addNumbers(5, 3)); // 8 — runs as native machine code
10
11
12// ❌ JIT-unfriendly — inconsistent types (deoptimizes)
13function mixedAdd(a, b) {
14 return a + b; // sometimes number+number, sometimes string+number
15}
16
17for (let i = 0; i < 500; i++) {
18 mixedAdd(i, i + 1); // V8 assumes: number + number
19}
20mixedAdd('hello', 5); // DEOPT! — assumption broken, falls back to bytecode
21// V8 now treats the function as polymorphic — slower

Memory — Stack and Heap

Memory — Stack and Heap

The JavaScript engine manages two areas of memory: the **Call Stack** and the **Heap**. Understanding both is key to understanding how JavaScript stores and accesses data.
Call Stack
What it stores:
Primitive values
(number, string, boolean,
null, undefined, symbol)
Function call frames
Local variables
Return addresses
How it works:
LIFO — Last In, First Out
Fixed size per frame
Auto-managed (push/pop)
Speed: Very fast
Size: Limited (~1MB)
Error: Stack Overflow
(too many nested calls)
VS
Heap
What it stores:
Objects {}
Arrays []
Functions
Closures
Everything non-primitive
How it works:
Unstructured memory pool
Engine allocates on demand
Garbage Collector frees
unused objects
Speed: Slower (GC overhead)
Size: Large (limited by RAM)
Error: Out of Memory
(too many live objects)
javascript
1// STACK — primitives stored by value
2let x = 10; // x stored on stack
3let y = x; // y gets a COPY of x's value
4y = 20;
5console.log(x); // 10 — x is unchanged
6console.log(y); // 20 — independent copy
7
8// HEAP — objects stored by reference
9let obj1 = { name: 'Alice' }; // object on heap, obj1 holds reference
10let obj2 = obj1; // obj2 copies the REFERENCE, not the object
11obj2.name = 'Bob';
12console.log(obj1.name); // 'Bob' — same object!
13console.log(obj2.name); // 'Bob'
14
15// CALL STACK — function frames
16function outer() {
17 let a = 1; // pushed onto stack
18 function inner() {
19 let b = 2; // pushed onto stack
20 return a + b; // reads a from outer frame
21 } // inner frame popped off stack
22 return inner();
23} // outer frame popped off stack
24
25console.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

Unlike C or C++, JavaScript does not require you to manually free memory. The engine's **Garbage Collector (GC)** automatically finds and removes objects that are no longer reachable from your code.

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.

javascript
1// ✅ Object becomes unreachable → eligible for GC
2function createData() {
3 let bigArray = new Array(1000).fill('data'); // allocated on heap
4 return bigArray[0]; // only returning first element
5 // bigArray itself has no external reference after function returns
6 // → GC will collect it
7}
8console.log(createData()); // 'data'
9
10
11// ❌ Memory Leak — event listener never removed
12const button = document.createElement('button');
13const largeData = new Array(10000).fill('important');
14
15function handleClick() {
16 // This closure captures 'largeData'
17 console.log(largeData[0]);
18}
19
20button.addEventListener('click', handleClick);
21// If button is removed from DOM but handleClick is never removed,
22// largeData stays in memory forever
23
24// ✅ Fix: always remove listeners you no longer need
25// button.removeEventListener('click', handleClick);
26
27console.log('Reachability demonstrated');

Inside V8 — A Closer Look

Inside V8 — A Closer Look

V8 is the most widely deployed JavaScript engine in the world. It powers Chrome, Node.js, Deno, Bun, and Edge. Understanding its architecture helps you write faster JavaScript.
V8 ComponentRoleDetails
IgnitionInterpreter / Bytecode GeneratorConverts AST to bytecode. Executes immediately. Also profiles execution to find hot functions.
TurboFanOptimizing JIT CompilerCompiles hot bytecode to optimized machine code. Uses type feedback from Ignition.
LiftoffBaseline Compiler (WebAssembly)Fast first-tier compiler for WebAssembly modules.
OrinocoGarbage CollectorGenerational, incremental, concurrent GC. Minimizes pause times.
TorqueInternal LanguageDSL used to write V8's built-in JS methods (Array.map, etc.) in a type-safe way.
MaglevMid-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.

javascript
1// ✅ Fast — same shape, V8 uses one hidden class for all
2class Point {
3 constructor(x, y) {
4 this.x = x; // always in same order
5 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 access
12
13
14// ❌ Slow — different shapes, V8 creates multiple hidden classes
15const a = {};
16a.x = 1; // hidden class A: { x }
17a.y = 2; // hidden class B: { x, y }
18
19const 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 properties
23// → V8 cannot optimize property access the same way
24
25console.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?

The **engine** and the **runtime** are often confused. They're related but distinct concepts.
⚙️

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

When the browser loads a webpage, it coordinates the JS engine with its own systems — the renderer, Web APIs, and the Event Loop.
1

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

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

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

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

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

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.

javascript
1console.log('1 — Start'); // Runs immediately (synchronous)
2
3setTimeout(() => {
4 console.log('3 — setTimeout'); // Runs LAST — macrotask queue
5}, 0);
6
7Promise.resolve().then(() => {
8 console.log('2 — Promise.then'); // Runs BEFORE setTimeout — microtask queue
9});
10
11console.log('1 — End'); // Runs immediately (synchronous)
12
13// Output order:
14// 1 — Start
15// 1 — End
16// 2 — Promise.then ← microtask queue (runs before next macrotask)
17// 3 — setTimeout ← macrotask queue
18
19// WHY? The event loop empties the microtask queue (Promises)
20// completely before moving to the next macrotask (setTimeout)

Test Your Knowledge

Test Your Knowledge

Quick Check

What is the correct order of stages in a modern JavaScript engine pipeline?

Quick Check

What is JIT compilation?

Quick Check

Where are JavaScript objects and arrays stored in memory?

Quick Check

Which of these is provided by the browser runtime, NOT the ECMAScript engine?

Quick Check

What is a 'deoptimization bailout' in JIT compilation?

Engine Performance Tips

Engine Performance Tips

Pro 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.
What's Next?
🔁

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.

Continue Learning