Lesson 13 · 16 min read
JS Engine and V8 Architecture
Parser, interpreter (Ignition), compiler (TurboFan), memory heap and garbage collection — what the engine does between your file and the CPU. Watch a function climb V8's tiers and fall back on a deopt, and step through mark-and-sweep.
On this page
- Concept
- Key words in plain English
- Why this matters
- The kitchen — an analogy
- The JS Engine vs. the JS Runtime Environment
- Step 1 — Parsing: tokens and the AST
- Step 2 — Compilation: why JS is JIT-compiled
- Beyond Ignition and TurboFan — the extra tiers
- Why property access is fast — hidden classes
- Step 3 — Execution: heap, stack, and Orinoco
- Garbage collection, properly explained
- See it live — feel the JIT warm-up
- Common misconceptions
- Interview questions
Concept
A JavaScript engine is a program that takes your JavaScript source code — plain text — and actually runs it. It does this in three broad steps: parsing the text into a structure it can understand, compiling that structure into something the CPU can execute quickly, and executing it while managing memory as it goes. Google's V8 (used in Chrome and Node.js) is the engine we'll use as the concrete example throughout, since it's the one you're running JavaScript on every single day.
Key Words In Plain English
- JavaScript engine is a program that reads JavaScript text and runs it. V8 (Chrome, Node.js), SpiderMonkey (Firefox) and JavaScriptCore (Safari) are engines.
- Runtime environment is the engine plus the extra tools the host adds, such as timers,
fetch, the DOM and the event loop. The host is the browser or Node.js. Difference in one line: the engine runs the language; the runtime adds the things around it. - Parsing is reading source text and turning it into a structured form. Token is one small piece of the text, such as a keyword, a name or a number. AST (Abstract Syntax Tree) is a tree that shows the structure of the code.
- Interpreter is a program that runs code step by step without first making machine code for all of it. Compiler is a program that translates code into another form, such as machine code. Machine code is the instructions the CPU runs directly. Bytecode is a compact set of instructions for a simple virtual machine. It is smaller and easier to run than source text.
- JIT (Just-In-Time) compilation is compiling code while the program runs, only for the parts that run often.
- Hot code is code that runs many times, so making it faster pays off.
- Optimization is making code run faster by using assumptions, such as “this function always gets numbers”. Deoptimization is throwing away the fast version when an assumption turns out wrong.
- Call stack is the list of function calls running right now. Heap is the large area of memory where objects, arrays and functions are stored.
- Hidden class (shape) is the engine's internal description of an object's property names and their positions. Objects with the same shape share one description. Polymorphic means a piece of code sees a few different shapes. Megamorphic means it sees many.
- Garbage collection (GC) is the engine automatically freeing memory for objects that nothing can reach any more. Reachable means you can get to the object by following references from a root. Root is a starting point for that search, such as a global variable or a variable on the call stack.
- Memory leak is memory your program no longer needs but still keeps, because something still points to it.
- Web API is a feature that the browser provides, not the language, such as
setTimeout,fetchand the DOM.
Why This Matters
- It explains why execution contexts, the call stack, and closures behave the way the earlier lessons described — they're not arbitrary rules, they're how the engine is actually built.
- It explains real performance advice you've probably heard without knowing why it's true — like “don't change an object's shape after creating it.”
- It sets up Lesson 14: the event loop isn't part of the JS engine at all — you can't understand where the engine's job ends and the browser's job begins until you've seen the engine's actual boundary.
The Kitchen — An Analogy
A guest hands the waiter a spoken order. The waiter can't cook from spoken words directly — first it gets written down as a structured order ticket: dish, quantity, modifications, in a fixed format the kitchen understands. That's parsing — turning free-form text into a structured representation.
A line cook picks up the ticket and starts cooking immediately, step by step, exactly as written — fast to start, but not the fastest possible way to cook if this exact dish keeps coming in. That's the interpreter.
Now imagine the head chef notices the kitchen has made the exact same complicated dish forty times tonight. Instead of re-reading the ticket line-by-line every time, the chef writes a pre-optimized prep sheet for that specific dish — precomputed steps, no wasted motion — and hands it to the line cook for every future order of that same dish. That's the optimizing compiler, kicking in only for the “hot,” frequently-run parts of your code.
The JS Engine vs. The JS Runtime Environment
This distinction matters and is easy to blur: the JS Engine (V8) only contains the parser, compiler/interpreter, the call stack, and the memory heap — the parts that actually understand and run JavaScript syntax. The JS Runtime Environment is the bigger container around it — the engine plus everything the browser (or Node.js) bolts on: Web APIs like setTimeout and fetch (Lesson 5, Lesson 12), the callback queue, the microtask queue, and the event loop that ties it all together (full detail in Lesson 14).
setTimeout, DOM APIs, and fetch are not part of the JS engine — the engine has no idea what a timer or a webpage even is. They're provided by whatever environment is hosting the engine, and handed to your code through the global object (window in browsers), exactly as Lesson 5 described.
Step 1 — Parsing: Tokens And The AST
let a = 7;Before anything runs, the engine's parser breaks this line into tokens — let, a, =, 7, ; — the smallest meaningful pieces of syntax. Then it arranges those tokens into an Abstract Syntax Tree (AST): a structured, tree-shaped representation of what the code actually means, roughly like this (this is the common ESTree format that tools show; V8's own internal tree differs in its details):
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": { "type": "Identifier", "name": "a" },
"init": { "type": "Literal", "value": 7 }
}
],
"kind": "let"
}Every later step — interpreting, compiling, optimizing — works off this tree, not the raw text. You can generate an AST for any snippet yourself at astexplorer.net to see this structure firsthand.
Step 2 — Compilation: Why JS Is JIT-Compiled
A common myth is that JavaScript is “just interpreted.” It isn't — modern engines use JIT (Just-In-Time) compilation, which blends two older approaches:
| Approach | Strength | Weakness |
|---|---|---|
| Pure interpreter | Starts running instantly, line by line | Re-does the same work every time a line re-runs — slow for hot loops |
| Pure ahead-of-time compiler | Produces fast machine code | Has to fully compile everything upfront before anything runs — slow to start |
| JIT (both together) | Starts instantly and gets fast where it counts | More moving parts internally |
In V8, this plays out as two named components working simultaneously, not one after the other:
- Ignition — the interpreter. It converts the AST into bytecode and starts executing immediately, function by function, with zero delay.
- TurboFan — the optimizing compiler. It watches which functions are called repeatedly (“hot” functions) and, in the background, compiles just those into highly optimized machine code — the “prep sheet” from the kitchen analogy.
If TurboFan's assumptions about a function's inputs turn out to be wrong later (say, a function that always received numbers suddenly receives a string), V8 deoptimizes — it throws away the optimized version and safely falls back to Ignition's bytecode for that function. This safety net is exactly what makes it safe for TurboFan to optimize aggressively in the first place.
Nothing is optimized up front. Ignition can start running the bytecode immediately — that's why JS starts fast.
- Ignitioninterpreter · bytecoderunning
- Sparkplugbaseline compiler
- Maglevmid-tier optimizer
- TurboFantop-tier optimizer
Beyond Ignition And TurboFan — The Extra Tiers
Modern V8 doesn't jump straight from “interpreted” to “fully optimized” — it added two intermediate tiers to smooth out the climb, so that mildly-warm functions aren't stuck paying interpreter overhead while waiting to become “hot enough” for full optimization:
| Tier | Role | Roughly when it starts |
|---|---|---|
| Ignition | Baseline interpreter — always runs first | Every function, immediately |
| Sparkplug | A fast, non-optimizing compiler that removes interpreter overhead | Soon, after a function has run a few times |
| Maglev | A mid-tier optimizer for functions that are warm but not yet “hot” | After a function has run a fair amount |
| TurboFan | The top-tier optimizer, generating near-native machine code | After a function has run a lot |
The exact thresholds are not call counts. V8 measures how much work a function has done (calls and loop turns) and changes its rules between versions, so treat the table as an order, not as numbers. You don't need to memorize these to understand JavaScript's execution model — Ignition and TurboFan cover the core idea perfectly well. But knowing they exist explains why V8's real-world performance keeps improving version after version without you changing a single line of your own code.
Why Property Access Is Fast — Hidden Classes
JavaScript objects are dynamic — you can add or remove properties at any time. Naively, that should make obj.roomNumber as slow as a hash-table lookup every single time. V8 avoids that with hidden classes (internally called Maps, or “shapes”): whenever you create objects the same way, V8 quietly gives them the same hidden class, recording exactly which properties exist and at what fixed offset — turning property access into a near-instant, direct memory read instead of a lookup.
function makeRoom(number, guest) {
this.number = number; // same order, every time...
this.guest = guest; // ...means every room shares one hidden class
}
const room1 = new makeRoom(305, "A. Sharma");
const room2 = new makeRoom(306, "R. Iyer");
// room1 and room2 share the same hidden class — fast, predictable accessBoth rooms point at Shape2. room.guest is “read slot 1” — one memory load, cached at the call site.
guest lives at a different slot in each shape, so every access must first check which shape it has. With many shapes the cache gives up (megamorphic).
If you add properties to different instances in a different order, or add properties long after creation, V8 has to create separate hidden classes for each shape — this is called going polymorphic (a few shapes, up to about four) or megamorphic (more than that), and it's a genuine, measurable performance cliff. This is the real, technical reason behind the common advice “initialize all of an object's properties in the constructor, in the same order, every time.”
Step 3 — Execution: Heap, Stack, And Orinoco
Execution needs two things you've already met, formally named here: the call stack (Lesson 2) tracks which function is currently running, and the memory heap is the much larger, less organized region where objects, arrays, and functions actually live. As a simple model, small values tied to a running function live on the stack, and anything that needs to outlive a single function call — including the variables a closure holds onto (Lesson 10) — lives in the heap. (Real engines are free to place values differently as an optimization; for example, strings are stored in the heap.)
Garbage Collection, Properly Explained
The heap fills up as your program creates objects, so the engine needs to reclaim memory from objects nothing references anymore. V8's collector is called Orinoco, and it works on a simple observation: most objects die young. So it splits the heap into two generations and treats them very differently:
| Generation | Holds | Algorithm |
|---|---|---|
| Young generation | Newly created, mostly short-lived objects | Scavenger — copies survivors between two small spaces; frequent, very fast |
| Old generation | Objects that survived long enough to be “promoted” | Mark-Sweep-Compact — walks the object graph from the roots (explained just below), marks what's reachable, frees the rest; runs less often, mostly in the background |
At its core, Mark-and-Sweep is the idea to hold onto: starting from your program's “roots” (global variables and the variables on the current call stack), then follows every reference from them — including the variables kept by closures that are still reachable —, the collector marks every object it can reach as alive. Everything left unmarked is unreachable garbage — nothing in your code can possibly get to it anymore — and its memory is freed. This is precisely why closures (Lesson 10) can keep a Lexical Environment alive indefinitely: as long as a reachable function still points to it, it's marked “alive” and never swept.
Some objects are still in use, and two are left over from an old booking. They point at each other — a reference cycle — but nothing else points at them.
A simpler idea — free an object when nothing points to it any more (reference counting) — fails on cycles: two objects that point only at each other never reach a count of zero, so they would leak forever. Mark-and-sweep never asks “does anything point to this?”, only “can a root reach this?”, so cycles are freed like anything else. The usual real-world leaks in JavaScript are therefore reachable garbage: a forgotten event listener, a growing array or map on a global, or a timer that keeps a closure alive.
See It Live — Feel The JIT Warm-Up
function square(n) {
return n * n;
}
console.time("cold run");
for (let i = 0; i < 5_000_000; i++) square(i);
console.timeEnd("cold run");
console.time("warm run");
for (let i = 0; i < 5_000_000; i++) square(i);
console.timeEnd("warm run");Paste this into the console
Run it in Chrome DevTools or Node.
Compare the two timings
The numbers vary by machine and engine version. The second loop is often a little faster, and sometimes the same, because this function is so small that V8 optimizes it very early, even during the first loop. Run the second loop a third time to see more of the effect.
Why
By the time the first loop finishes,
squarehas been called millions of times — hot enough that V8 has very likely compiled it into optimized machine code. Later runs reap the benefit. Warm-up is real, but for tiny code it is hard to see with a simple timer.
Common Misconceptions
“JavaScript is purely interpreted, unlike compiled languages.” Modern engines JIT-compile it — an interpreter starts execution instantly, and a compiler optimizes the hot paths in the background, at the same time.
“The event loop and callback queue are part of the JS engine.” They're not — they belong to the surrounding runtime environment (the browser or Node.js), not to V8 itself. Full detail in Lesson 14.
“Garbage collection only runs when the program is about to run out of memory.” V8 starts a collection when a memory area reaches its size limit, long before the machine is out of memory. Much of the work is split into small steps or done on other threads, so that your code is paused as little as possible.
Interview Questions
What are the three broad steps a JS engine takes to run your code?
Parsing (source code → AST), compilation (AST → bytecode, with hot paths further optimized into machine code), and execution (using the call stack and memory heap, with the garbage collector reclaiming unused memory).
What is an AST, and why does the engine build one?
An Abstract Syntax Tree — a structured, tree-shaped representation of what the source code means. Every later step (interpreting, compiling, optimizing) operates on this tree rather than on raw text.
What does JIT compilation mean, and why does V8 use it?
Just-In-Time compilation combines an interpreter (fast to start) with a compiler (fast to run) so code begins executing immediately while frequently-run ("hot") functions get optimized into fast machine code in the background.
Name V8's interpreter, its top-tier optimizing compiler, and its garbage collector.
Ignition (interpreter), TurboFan (optimizing compiler), and Orinoco (garbage collector).
What is Mark-and-Sweep, in one sentence?
Starting from reachable roots, the collector marks every object it can still reach as alive, then frees the memory of everything left unmarked.
What is deoptimization, and what usually triggers it?
When optimized machine code's assumptions stop holding — most often a function suddenly receiving a different type or object shape than before — V8 throws the optimized code away and continues in Ignition's bytecode. It keeps the program correct at the cost of speed, and the function may be re-optimized later for the wider set of types.
Why should you create objects with the same properties in the same order?
So they share one hidden class. Property access can then be a single read from a fixed slot, cached at each call site. Different orders or late-added properties create several shapes, making access polymorphic or megamorphic and noticeably slower.
Can two objects that reference each other leak memory in JavaScript?
Not by themselves. Mark-and-sweep frees anything a root can't reach, cycles included. Leaks in practice are objects that are still reachable by accident — forgotten listeners, timers, caches or globals that keep growing.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series