The Runtime: Engine, Stack and Memory
From source to running code
Parse, bytecode, JIT
JavaScript is not executed line by line. An engine — V8 in Chrome and Node, SpiderMonkey in Firefox, JavaScriptCore in Safari — processes your file in stages:
Parse: the text becomes a syntax tree. Failures here are lesson 14's
SyntaxErrors — nothing has executed yet. Interpret: the tree
becomes bytecode that runs immediately. Optimize: while the code runs, the
engine watches which types actually arrive and compiles "hot" functions into machine code —
the just-in-time (JIT) compiler at work.
// The engine cannot know at parse time what `a + b` does — JS has no static types.
// So it SPECULATES: "these were numbers last time — compile for numbers".
function add(a, b) {
return a + b;
}
for (let i = 0; i < 1_000_000; i++) add(i, i); // hot: JIT compiles a number-fast path
console.log(add(3, 4)); // 7 — the fast path handles numbers
console.log(add("3", "4")); // "34" — a string showed up; the assumption is broken
// Expected output:
// 7
// 34
That last call deoptimizes: the engine throws away the specialized version and falls back to the general path. Nothing crashes — but the practical rule stands: keep the types a function sees stable, and it stays fast.
The call stack
Frames: one per call, last in / first out
Every call creates a frame holding that invocation's local variables and its return address. Frames stack while calls nest and pop off as functions return — which is why lesson 14's error list is called a stack trace: it is the current stack, printed.
function descend(depth) {
if (depth === 0) return "bottom"; // base case: the stack stops growing here
return descend(depth - 1); // each frame stays alive while it waits
}
console.log(descend(3)); // depth 1 → bottom — after building 4 frames, unwinding them
While descend(1) runs, three frames sit on the stack; the heap object is shared by two references.
Stack overflow — the stack has a hard limit
function runaway(n) {
return runaway(n + 1); // no base case: every frame waits on the next
}
runaway(0);
// RangeError: Maximum call stack size exceeded
Deep recursion is the everyday cause (demo runtime/23_runtime_lab.js catches and
prints this exact error). The fix is a reachable base case — or a loop, which reuses one frame
instead of stacking thousands.
The heap and references
Frames are small and predictable; objects are not. Objects — arrays, functions, class instances — live in the heap, and variables hold only references to them. Lesson 04's value-versus-reference diagram is a picture of exactly this. Closures live here too: a returned function keeps its captured variables in the heap long after its own frame is gone.
Garbage collection
Reachability, not tidiness
There is no free() in JavaScript. The collector periodically frees every object
that is unreachable — nothing in scope can lead to it, directly or transitively.
This is why removing an element only helps if it also makes the object unreachable.
Three everyday leak patterns
// 1. Unbounded caches: every entry stays reachable, so nothing is ever collected
const cache = [];
cache.push({ key: "a", payload: new Array(1000).fill(0) });
// 2. Forgotten timers: the callback closes over big data, the timer keeps it alive
const bigData = new Array(100_000).fill("x");
setInterval(() => console.log(bigData.length), 1000); // runs forever unless cleared
// 3. Detached nodes: a removed element stays alive via a saved reference
let detached = document.querySelector("#widget"); // (browser code)
document.querySelector("#widget")?.remove(); // out of the DOM…
console.log(detached !== null); // …but alive in the heap,
// with every listener attached
Shape matters: hidden classes
Engines do not store property names in dictionaries — that would be slow. Objects with the same properties, created in the same order, share a hidden class and run fast, specialized code. Objects that drift (properties added one at a time, in varying orders) push the engine into slower generic paths:
// Same properties, SAME order → one shape
const point1 = { x: 1, y: 2 };
const point2 = { x: 3, y: 4 };
// Drifted shape: properties added one at a time, in a different order
const point3 = { y: 5 };
point3.z = 9;
point3.x = 7;
The practical habit: build objects with all their properties up front, in a consistent order — constructors and object literals do this naturally.
Practice: make the invisible visible
The task
Run runtime/23_runtime_lab.js with node: it builds and unwinds
frames, overflows the stack on purpose, contrasts stack and heap semantics, and grows a leak
you can measure. Predict each section before running. Then delete the base case in
descend, predict the error, and check.
The checklist
- You can name the pipeline stages: parse → bytecode → JIT optimize (→ deoptimize).
- You can explain a stack trace as "the current call stack, printed".
- You know objects live in the heap and assignment copies references, never objects.
- You can name two leak patterns and say what keeps each one reachable.