← All guides

How JavaScript Works

Everything you need to understand what JavaScript is really doing, on one page and in order. No prior knowledge beyond writing a little JavaScript is assumed — 25 short chapters, each with an animation.

Press Play on any animation, or use Next and Back to go at your own pace. Mark chapters as done to track where you are — progress is saved in this browser.

0 of 25 chapters donesaved in this browser only
  1. Part 1 · 6 chaptersHow your code runsWhat happens between saving a file and seeing it run.
  2. Part 2 · 3 chaptersMemoryWhere your data lives, and how the engine cleans up after you.
  3. Part 3 · 4 chaptersValues and objectsHow values convert, how objects share behaviour, and how the engine keeps them fast.
  4. Part 4 · 6 chaptersAsynchronous JavaScriptHow a single-threaded language waits for timers, networks, and users.
  5. Part 5 · 5 chaptersThe browserTurning code into pixels, and keeping the page responsive.
  6. Part 6 · 1 chapterBeyond the browserThe same engine on a server, moving more data than fits in memory.

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.

03 Variables: let, const, and var

A variable is a name for a value. let and const live inside the nearest { } block; the older var ignores blocks and belongs to the whole function, which is behind one of JavaScript's most famous bugs.

if (true) {
var a = 1
let b = 2
}
console.log(a)
console.log(b)
 
for (var i = 0; i < 3; i++) setTimeout(() => log(i))
for (let j = 0; j < 3; j++) setTimeout(() => log(j))
 
const user = { name: 'Ada' }
user.name = 'Grace'
user = {}
variables
a (var)1 · whole function
b (let)2 · this block only
output
—

var belongs to the whole function (or the whole script). let and const belong to the nearest { } block. Inside the block both exist.

0 / 6
Key idea: Use const by default, let when the value must change, and never var in new code.

04 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.

05 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.

06 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?

Part 2

Memory

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

07 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.

08 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.

09 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.

Part 3

Values and objects

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

10 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.

11 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.

12 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.

13 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.

Part 4

Asynchronous JavaScript

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

14 Callbacks

Functions are values, so you can pass one to another function to be called later. Some callbacks run straight away — like the ones map and forEach take — and others run when something finishes, like a timer or a click — and JavaScript carries on in the meantime.

function greet(name, done) {
console.log('Hi ' + name)
done()
}
greet('Ada', () => console.log('greeted'))
 
console.log('A')
setTimeout(() => console.log('B'), 1000)
console.log('C')
output
—
waiting
—

A callback is just a function you pass to another function, for it to call later. greet takes a name and a function called done.

0 / 6
Key idea: Pass the function, don't call it. Async callbacks run later — everything below runs first.

15 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.

16 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.

17 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.

18 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.

19 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.

Part 5

The browser

Turning code into pixels, and keeping the page responsive.

20 The DOM

The browser turns your HTML into a tree of objects — the DOM — and that tree is what's on screen. JavaScript finds nodes with selectors, changes their properties, creates and removes them, and listens for events that bubble up the tree.

HTML (text)
<body> <h1>Groceries</h1> <ul id="list"> <li>Milk</li> <li>Eggs</li> </ul> </body>
DOM tree (objects)
not built yet

HTML is just text. The browser reads it top to bottom and builds a tree of objects in memory — the DOM (Document Object Model). That tree, not the text, is what you see and what JavaScript changes.

0 / 6
Key idea: HTML is text; the DOM is the live tree built from it. Change the tree and the page changes.

21 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.

22 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.

23 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.

24 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.

Part 6

Beyond the browser

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

25 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.
That's the whole tour. Next, see How the Web Works, the TypeScript articles for adding types on top, or the DSA path for algorithms.