Part 1

How your code runs

What happens between saving a file and seeing it run.

01 The engine: from text to machine code

A browser or Node doesn't run your file as-is. A JavaScript engine reads it, turns it into a tree, runs it quickly in an interpreter, and then compiles the parts that run a lot into fast machine code — making bets about your types as it goes.

Source
Parser → AST
Interpreter
Optimizing compiler
your code
function add(a, b) { return a + b } for (let i = 0; i < 10000; i++) add(i, 1) add('a', 'b')

To the engine, a script starts as a string of characters. Nothing has run yet. Engines like V8 (Chrome, Node), SpiderMonkey (Firefox) and JavaScriptCore (Safari) all take it through the same broad stages.

0 / 6
Key idea: Code starts slow and gets faster the more it runs. Keeping the types that flow through a function consistent keeps it fast.

02 The call stack

JavaScript runs one thing at a time. It tracks where it is with a stack: calling a function pushes a frame on top, returning pops it off. Whatever is on top is what's running right now. Too many frames and you get the famous Maximum call stack size exceeded.

factorial(3)
↑ top of stack

Call factorial(3). It needs 3 × factorial(2), so it pauses and pushes a new frame.

0 / 6
Key idea: One stack, one thing at a time. Everything else in this page builds on that.
Go deeper: Recursion and the Call Stack: What Actually Happens · 3 min read →

03 Hoisting and the temporal dead zone

Before any line in a scope runs, the engine creates every variable and function that scope declares. var starts as undefined, functions start ready to call, and let / const exist but can't be touched until their line runs.

creation phase

1console.log(count)
2greet()
3console.log(name)
4var count = 1
5let name = 'Ada'
6function greet() { console.log('hi') }
 

scope bindings

scanning…

Before a single line runs, the engine scans the scope and creates every binding it declares. "Hoisting" is not code moving upwards — it is bindings existing before execution starts.

0 / 6
Key idea: Nothing moves. Declarations are set up before execution — in different states.
Go deeper: Hoisting and the Temporal Dead Zone · 5 min read →

04 Scope and closures

Every function can see the variables where it was written, not where it's called. When a function outlives the function that created it, it keeps those variables alive — that's a closure, and it's how JavaScript does private state.

global scopemakeCounter() scopelet count = 0increment() scopecount++ … where is count?found one scope up

A variable lookup walks outward through the scope chain. The closure is increment() keeping makeCounter's scope alive after it returned.

Key idea: A function carries its birthplace with it.
Go deeper: Closures in JavaScript: Private State, Loops, and Stale Values · 5 min read →

05 The this keyword

Unlike scope, this is decided by how a function is called, every single time. Four rules cover almost everything, and arrow functions opt out of all of them.

const user = {
name: 'Ada',
hi() { return this.name },
}
const admin = { name: 'Grace' }
 
user.hi()
const hi = user.hi; hi()
hi.call(admin)
function Person(n) { this.name = n }
new Person('Linus')
user.later = function () {
setTimeout(() => this.name)
}
rule
—
this =
—
result
—

this is not decided where a function is written. It is decided each time the function is called, by how the call looks. Four rules cover almost every case.

0 / 5
Key idea: Look at the call site, not the definition: what is to the left of the dot?
Go deeper: The this Keyword: Four Rules That Explain Every Case · 4 min read →

Part 2

Memory

Where your data lives, and how the engine cleans up after you.

06 The stack and the heap

A running program uses two kinds of memory. The stack holds each function's local variables and disappears the moment the function returns. The heap holds objects, which live on for as long as something still points to them.

function makeUser() {
const age = 36
const user = { name: 'Ada', tags: ['dev'] }
return user
}
let u = makeUser()
u = null
stack
global
u<uninitialized>
heap
empty

Memory has two main areas. The stack holds one frame per running function, with its local variables. The heap is a big pool where objects live for as long as something points to them.

0 / 4
Key idea: Local variables live and die with their function. Objects live on the heap until nothing references them.
Go deeper: The Stack and the Heap: Where JavaScript Keeps Your Data · 4 min read →

07 Values, references, and copying

Numbers and strings are stored directly. Objects and arrays aren't — a variable holds a reference to them. Spread ({ ...obj }) copies only the top level, so nested objects stay shared between the copy and the original.

const user = { name: "Ada", address: { city: "Pune" } }
reachable from user
user #1
name"Ada"
address→ #2
address #2
city"Pune"
reachable from copy
—

Two objects on the heap: the user (#1) and the address it points to (#2). The address field does not contain the address — it holds a reference to it.

0 / 4
Key idea: Copying an object copies references one level deep. structuredClone copies all the way down.
Go deeper: Shallow vs Deep Copy in JavaScript, and What structuredClone Can't Copy · 4 min read →

08 Garbage collection

You never free memory yourself in JavaScript. Every so often the garbage collector starts from the roots — globals and the variables of running functions — marks everything it can reach, and frees the rest. Objects that only point at each other still get collected.

rootsuserprofileavatarsessionsocket
● reachable (marked)● unreachable

The heap is a graph: objects pointing at objects. The roots are where every reference chain starts — global variables and the local variables of functions currently on the stack.

0 / 5
Key idea: Reachable from a root = kept. Unreachable = freed. A memory leak is something you forgot is still reachable.
Go deeper: JavaScript Memory Management: How the Garbage Collector Thinks · 5 min read →

Part 3

Values and objects

How values convert, how objects share behaviour, and how the engine keeps them fast.

09 Type coercion

When an operator gets values of the wrong type, JavaScript quietly converts them. The rules are consistent, but they combine into famous surprises like '5' + 1 being '51'. Knowing the few rules — and using === — removes the mystery.

'5' + 1
→'5' + '1'
→'51'

When either side of + is a string, + means "join text". The number 1 is converted to the string "1", and the result is "51", not 6.

0 / 6
Key idea: + with a string joins text; every other math operator converts to numbers; == converts, === never does.
Go deeper: Type Coercion and Equality: Making Sense of == in JavaScript · 5 min read →

10 Prototypes

JavaScript objects don't copy methods from a class. Each object has a hidden link to another object, its prototype. When a property isn't found, the engine follows that link, and the next, until it finds it or reaches null. class is a nicer way to set up the same chain.

dog
{ name: 'Rex' }
[[Prototype]] ↑
Dog.prototype
{ bark() }
[[Prototype]] ↑
Animal.prototype
{ eat() }
[[Prototype]] ↑
Object.prototype
{ toString(), … }
[[Prototype]] ↑
null — top of the chain

We call dog.eat(). The engine needs to find an "eat" property. Press play to watch it walk the chain.

0 / 3
Key idea: Missing property? Walk up the chain. That walk is inheritance.
Go deeper: Prototypes and Inheritance: How JavaScript Objects Really Work · 6 min read →

11 Hidden classes and inline caches

Property lookups are fast because the engine quietly gives every object a hidden shape: a layout that says which property sits in which slot. Objects built the same way share a shape, and each property access remembers the shapes it has seen, so it can skip the lookup next time.

function Point(x, y) { this.x = x; this.y = y }
const p = new Point(1, 2)
const q = new Point(3, 4)
function getX(o) { return o.x }
getX(p); getX(q)
const r = { y: 5, x: 6 }
getX(r)
hidden shapes
S0 { }
objects
none yet
getX cache · empty
empty

Objects look like dictionaries, but looking a name up in a dictionary on every property access would be slow. So the engine gives each object a hidden "shape" describing its layout: which properties it has, and at which slot.

0 / 5
Key idea: Build objects the same way, in the same order, and property access stays fast.
Go deeper: Hidden Classes and Inline Caches: Why Object Shape Matters · 4 min read →

12 Immutability and structural sharing

Instead of changing an object, make a new one — but only copy the path that changed and reuse everything else. It's cheap, and it lets React and friends tell what changed with a simple ===.

version 1stateusersettingsnameemailthemelang

green = newly allocated · dashed = reused by reference

An immutable state tree. We want to change one leaf — the theme — without mutating anything.

0 / 5
Key idea: New object on the changed path, shared objects everywhere else.
Go deeper: Immutability and Structural Sharing · 4 min read →

Part 4

Asynchronous JavaScript

How a single-threaded language waits for timers, networks, and users.

13 The event loop

Timers, network requests and clicks are handled outside JavaScript. When they finish, their callbacks wait in a queue. The event loop moves the next one onto the call stack — but only when the stack is empty, and promise callbacks (microtasks) always go before timers (tasks).

Call stack
(empty)
Web APIs
(empty)
Microtask queue
(empty)
Task queue
(empty)
Console

Press Play (or step with Next) to watch the script run.

0 / 13
Key idea: Stack empty → run every microtask → run one task → repeat.
Go deeper: The JavaScript Event Loop: How Async Actually Works · 5 min read →

14 Promises

A promise is a placeholder for a value that isn't ready yet. It starts pending and settles exactly once — fulfilled with a value or rejected with an error. .then and .catch register what should happen next.

pendingresolve(value)reject(reason)fulfilledrejected

A promise settles exactly once — fulfilled or rejected — and never changes state again. Extra resolve/reject calls are silently ignored.

Key idea: Pending, then settled once and forever.
Go deeper: JavaScript Promises in Depth: Chaining, Errors, and Combinators · 5 min read →

15 async / await

await doesn't block anything. It pauses just this function, hands control back to the event loop, and resumes it as a microtask when the promise settles. The code reads top to bottom but runs in pieces.

Call stack
(empty)
Web APIs
(empty)
Microtask queue
(empty)
Task queue
(empty)
Console

Press Play (or step with Next) to watch the script run.

0 / 10
Key idea: await = "finish the rest of me later". Everything after it is a callback.
Go deeper: Async/Await Under the Hood: Promises in Disguise · 4 min read →

16 Generators

A generator is a function that can pause in the middle with yield and pick up later exactly where it stopped, local variables intact. It's the machinery async functions are built on.

function* counter() {
yield 1
yield 2
yield 3
}
output
(nothing yet)

const g = counter() — calling a generator runs NO code. It hands back a paused iterator. Generators are lazy.

0 / 4
Key idea: A function you can pause, and resume on demand.
Go deeper: Generators and Iterators: Functions That Pause · 4 min read →

17 Cancellation with AbortController

Promises can't be cancelled on their own. An AbortController gives you a signal you pass into fetch, listeners and your own code, so one call to abort() stops all of it.

controller.signalaborted: false
fetch /search?q=rerunning
fetch /suggestrunning
timeout 5srunning

cleanup performed

—

One AbortController produces one signal, and any number of operations can listen to it. Here three async operations share a single signal.

0 / 5
Key idea: Pass the signal everywhere; abort once to clean up everything.
Go deeper: AbortController: Cancellation That Actually Cleans Up · 4 min read →

Part 5

The browser

Turning code into pixels, and keeping the page responsive.

18 From DOM to pixels

After JavaScript changes the page, the browser recalculates styles, works out where every box goes (layout), paints them, and composites the layers. Changing size or position redoes layout; changing transform or opacity can skip straight to compositing.

(idle frame)
JS
→
Style
→
Layout
→
Paint
→
Composite
— 

Every visual change runs through the same pipeline. Which stages it reaches decides whether a frame costs microseconds or milliseconds.

0 / 6
Key idea: Style → layout → paint → composite. The earlier a change starts, the more it costs.
Go deeper: Reflow, Repaint, Composite: The Browser Rendering Pipeline · 4 min read →

19 Debounce and throttle

Scrolling, typing and resizing fire dozens of events a second. Debounce waits until they stop; throttle lets one through at a fixed rate. Both keep expensive work from running on every event.

Raw events0 calls
Debounced · 800 ms0 calls
Throttled · 800 ms0 calls

Mash the button in bursts, pause, then burst again. Debounce waits for silence; throttle fires immediately but at most once per 800 ms. The timeline covers 10 seconds, then starts over.

Key idea: Debounce: after the storm. Throttle: at a steady pace during it.
Go deeper: Debounce vs Throttle: Taming Event Storms in JavaScript · 4 min read →

20 Web Workers

The main thread runs your JavaScript and draws the page, so a long calculation freezes everything. A Web Worker runs code on a separate thread and talks to the page by passing messages.

everything on the main thread

main thread

heavy work in a worker

main thread

worker thread

green = frame painted · red = frame dropped · amber = worker computing · dark = postMessage

Two timelines, 16ms per slot. Green means the browser painted a frame on time; the page feels alive.

0 / 5
Key idea: Heavy work off the main thread, results back by message.
Go deeper: Web Workers: Getting Heavy Work Off the Main Thread · 4 min read →

21 How React updates the page

React re-runs your components, compares the new result with the last one, and applies only the differences to the DOM. In lists it matches items by key — so the key decides which item keeps which state.

key={index}
0Alice 
1Bob 
key={user.id}
aliceAlice 
bobBob 
■ reused, correct■ reused, wrong state■ newly created

Two copies of the same list of rows, each with an uncontrolled input. The only difference is the key: array index on the left, a stable id on the right.

0 / 4
Key idea: Same key = same component instance. Use ids from your data, not array indexes.
Go deeper: React Reconciliation: What Keys Are Actually For · 4 min read →

Part 6

Beyond the browser

The same engine on a server, moving more data than fits in memory.

22 Streams and backpressure

Node reads big files and network data in chunks instead of all at once. When the reader is faster than the writer, backpressure tells it to pause, so memory stays flat no matter how large the data is.

no backpressure

producer running

producer
consumer

buffered 0 · produced 0 · consumed 0

with backpressure

producer running

producer
consumer

buffered 0 · produced 0 · consumed 0

A producer reads rows far faster than the consumer can write them. Top: no backpressure. Bottom: the producer waits for the consumer.

0 / 5
Key idea: Process data in chunks, and let the slowest step set the pace.
Go deeper: Streams and Backpressure: Why Your Node Process Runs Out of Memory · 4 min read →
That's the whole tour. If you want to keep going, the JavaScript articles cover each topic in depth, and the DSA path does the same for algorithms.