Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
A1

Engine & memory

What V8 is actually doing while your code just runs.

Everything so far has been the language as you write it. This chapter is what the engine does with it — and it's the layer most senior-level interviews actually probe, because it's the layer where "it works" and "it works well" stop being the same question.

Stack vs heap

call stack — fixed-size frames, LIFO
main()x = 5
makePoint()x = 1, y = 2
a frame's local primitives live right here
heap — dynamic, garbage-collected
Point { x: 1, y: 2 }0x7a2f…
the stack only ever holds a REFERENCE to this

This is the exact primitive-vs-reference split from the types chapter, one level lower: a primitive that never leaves its function lives directly in that function's stack frame — cheap to allocate, cheap to reclaim, gone the instant the frame pops. An object always lives on the heap, and the stack only ever holds a pointer to it — which is the entire mechanical reason copying a variable copies the reference and not the data.

⚠ The real engine is smarter than this diagram V8 actually runs escape analysis — if it can prove an object never leaves the function that creates it, it may stack-allocate that object anyway, and a captured primitive can get promoted onto the heap as part of a closure's context. The stack/heap split above is the correct mental model for reasoning about your code; the engine's actual placement decisions are an optimization detail on top of it, not a contradiction of it.

Each function call gets its own execution context — the formal name for what's been informally called a "scope" in every chapter so far. It bundles an environment record (the actual variable bindings) with a reference to the outer context, and that chain of outer references is the scope chain from two chapters back. A closure, mechanically, is just a function holding onto a reference to an execution context that would otherwise have been popped off the stack and discarded.

Garbage collection

JS never frees memory by counting references down to zero the moment they drop — it periodically asks a different question: reachability. Starting from a set of roots (global variables, everything currently on the call stack), the collector walks every reference it can find. Anything it never reaches is garbage, full stop — not "has zero references," which matters the instant two objects reference only each other.

function makeCycle() {
  const a = {};
  const b = {};
  a.friend = b;
  b.friend = a;      // a and b reference EACH OTHER
  return "created a cycle";
}
console.log(makeCycle());
// once makeCycle() returns, nothing on the stack points to a or b anymore —
// they're unreachable from any root, cycle or not, and get collected

A reference-counting collector (like older versions of Python) would actually leak this — a and b each hold one reference to the other, so neither ever hits zero on its own. Reachability-based collection sidesteps that entire class of bug for free: once makeCycle returns, nothing reachable from a root points at either object, cycle or not, so both are simply gone.

V8 specifically runs a generational collector, built on one observation: most objects die young. New objects go into a small "young generation" that gets swept frequently and cheaply (Scavenger); anything that survives a few sweeps gets promoted to the "old generation," which is collected far less often, using a slower mark-and-sweep (mark everything reachable, then sweep away everything that wasn't marked) with an occasional mark-compact pass to defragment. Optimizing for the common case — short-lived objects — instead of treating every object identically is most of where the speed comes from.

Memory leaks — JS still has them

"Garbage collected" means unreachable memory gets freed automatically. It does not mean memory can't leak — it means every JS leak is really the same root cause: something is still reachable that the program no longer actually needs.

Pattern What keeps it reachable
A forgotten setIntervalthe timer itself holds a live reference to its callback and everything that callback closes over, forever, until clearInterval
A detached DOM noderemoved from the page, but still referenced by a JS variable or an event listener you forgot to remove — the node itself, and everything it references, stays alive
An unbounded cachea plain Map used as a cache that only ever grows — every entry is reachable through it forever, since nothing ever calls .delete()
A closure over something hugea small, long-lived closure that happens to reference one variable from a scope containing something large — the ENTIRE execution context stays alive to keep that one binding around
// The fix for the cache row above — cap it, or use a WeakMap when the
// key's natural lifetime should decide the entry's lifetime (I2 covered this)
const cache = new Map();
function memoizedButBounded(key, compute) {
  if (cache.has(key)) return cache.get(key);
  if (cache.size >= 500) cache.delete(cache.keys().next().value);   // evict oldest
  const value = compute();
  cache.set(key, value);
  return value;
}
Rule Every leak is a lifetime mismatch: some reference is living longer than the data it points to should. Fixing a leak is almost always "stop something from holding a reference it no longer needs" — clear the interval, remove the listener, cap the cache, null out the field — never a special "free this memory" call, because JS has no such call.

Heap snapshots and allocation timelines

DevTools' Memory panel is how a suspected leak actually gets confirmed rather than guessed at: take a heap snapshot, perform the suspect action several times (open and close a modal, navigate back and forth), take another snapshot, and compare. An object count that keeps climbing across that comparison — for a thing you'd expect to be fully cleaned up — is the leak, and the snapshot's retainer tree shows exactly what's still holding a reference to it. The allocation timeline view is the same idea over time instead of two fixed points — useful for catching steady growth during normal use rather than one specific suspected action.

Hidden classes and inline caches

V8 doesn't store objects as generic key/value hash maps the way this sentence probably makes you picture — it dynamically builds a hidden class (an internal, fixed layout) for every distinct shape of object it sees, and every object with that same shape shares the same hidden class.

function Point(x, y) { this.x = x; this.y = y; }

const a = new Point(1, 2);   // x then y — hidden class C0
const b = new Point(3, 4);   // x then y — SAME hidden class C0, shares it with a

const c = new Point(5, 6);
c.z = 7;                      // now c has a DIFFERENT shape — its own hidden class C1

A property access like point.x compiled at a specific call site gets an inline cache: after the first call, V8 remembers "the object at this call site had hidden class C0, and its x was at this exact offset" — so the next call with the same hidden class skips property lookup entirely and reads straight from that offset.

Term Means Speed
Monomorphica call site has only ever seen one hidden classfastest — the inline cache is a direct hit every time
Polymorphica call site has seen a handful (2-4) of different hidden classesstill fast — checks a short list
Megamorphica call site has seen too many shapes to trackslow — V8 gives up on the inline cache and falls back to a generic lookup
⚠ This is genuinely hard to see with a stopwatch It's tempting to prove this with performance.now() around a quick loop — in practice, allocation cost, garbage collection pauses, and JIT warm-up noise routinely swamp the actual effect at small scale, and a rushed 3-line "benchmark" is exactly how people ship confidently wrong performance conclusions. The takeaway isn't "go measure this" — it's the practical rule below, which holds regardless of what any one quick timing run happens to show.
Rule Build objects of the same "kind" with their properties assigned in the same order, every time — ideally all in the constructor, none bolted on conditionally afterward. Shape consistency is what keeps a hot call site monomorphic; it's a real, well-documented V8 optimization concern, not premature optimization folklore.

JIT and deoptimization

V8 starts running everything through Ignition, a fast-starting interpreter — there's no compile pause before your code runs at all. A function called enough times gets handed to TurboFan, the optimizing compiler, which compiles it down to fast machine code under the assumptions it's observed so far — including the hidden classes and argument types it's seen at every call site inside it.

Break one of those assumptions — a function optimized for numbers suddenly gets called with a string, a monomorphic call site starts seeing a new shape — and V8 deoptimizes: throws away the compiled machine code and drops back to the slower interpreter for that function, at least until it can safely re-optimize with the new reality accounted for. A function that gets optimized, called differently, deoptimized, called differently again, and re-optimized in a loop never settles into its fast path at all.

Say it like this → "Predictable shapes and stable argument types aren't just a style preference — they're what let TurboFan's assumptions hold, which is what keeps a hot function compiled instead of bouncing back to the interpreter every time something unexpected shows up at one of its call sites."
Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

Attach data without leaking5 tests · advanced
←previousOffline & storage↑ CovernextAdvanced async→