JavaScriptHow JavaScript Works

How JavaScript Works

When you write a line like alert("Hello"), what actually happens between pressing Refresh and seeing the dialog? You don't need to know the full answer to be productive — but a clear mental model of what is running your code turns a lot of mysteries into predictable behaviour.

The JavaScript engine

A JavaScript engine is the program that actually reads your code and runs it. Different browsers ship different engines, and they all implement the ECMAScript specification:

  • V8 — Google Chrome, Microsoft Edge, Opera, Node.js, Deno, Cloudflare Workers, Bun (parts of it).

  • SpiderMonkey — Firefox. The first JavaScript engine ever shipped, written by Brendan Eich himself.

  • JavaScriptCore (Nitro) — Safari, all iOS browsers.

  • Chakra — old Internet Explorer / pre-Chromium Edge.

From a developer's point of view, the engine differences mostly disappear: they all implement the same language. Performance and exact internals differ, but the code you write is the same.

From source code to running program

Modern engines do something more sophisticated than "interpret your code line by line". A simplified pipeline looks like this:

  1. Parsing. The engine reads your source text and turns it into an Abstract Syntax Tree (AST) — a structured representation of your code. Syntax errors are caught here.

  2. Compilation. A baseline compiler turns the AST into bytecode or quick machine code so execution can start fast.

  3. Execution. The engine runs the compiled code. While it runs, it watches which functions are "hot" — used often.

  4. Just-in-Time optimisation. Hot functions are passed to an optimising compiler (V8 calls it TurboFan) that produces highly tuned native machine code based on the types it has actually seen at runtime.

  5. De-optimisation. If the assumptions stop holding — say a function that always took numbers suddenly receives a string — the engine quietly falls back to the slower bytecode and re-optimises later.

This blend of interpretation and compilation is why modern JavaScript can be both as flexible as a scripting language and fast enough to run games, IDEs and 3-D engines. You write JavaScript; the engine effectively writes machine code on your behalf.

The runtime: engine + host APIs

The engine alone cannot make an HTTP request, draw to the screen, read a file or wait three seconds. Those abilities come from the host environment:

  • In a browser — the engine is wrapped by the browser, which adds DOM APIs (the document, elements, events), window, fetch, timers, localStorage, canvas, audio, geolocation and dozens more.

  • In Node.js — the same V8 engine is wrapped by Node, which adds the file system, network sockets, processes, streams, OS utilities and CommonJS modules.

  • At the edge or in serverless — a smaller subset is wrapped to suit the platform.

So "JavaScript" in practice always means engine + environment. The same language runs everywhere; the surrounding APIs change.

Memory: the heap and the stack

At runtime the engine keeps two key memory regions:

  • The call stack — a stack of frames representing function calls in progress. Each frame holds local variables and a return location.

  • The heap — a larger, unstructured area where objects, arrays and closures live. Variables on the stack often hold references that point into the heap.

You never allocate or free heap memory yourself. JavaScript has automatic garbage collection: objects that nothing can reach any more are silently reclaimed.

Single-threaded execution

JavaScript runs on a single thread. There is only one call stack and only one piece of your code executing at any moment. That sounds limiting, but combined with non-blocking APIs and the event loop, it scales surprisingly well.

A blocking example

JS
function blockFor(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end) {
    // do nothing — but keep the thread busy
  }
}

console.log("start");
blockFor(2000);              // synchronous: blocks the thread for 2s
console.log("finished");

While blockFor runs, nothing else on the page can happen — clicks don't fire, animations freeze. That is the single-thread tax.

The event loop

To stay responsive, JavaScript hands slow tasks (network, timers, user input) to the host environment and gives them a callback to run later. When the engine's call stack is empty, the event loop picks the next callback from a queue and runs it.

Non-blocking equivalent

JS
console.log("start");

setTimeout(() => {
  console.log("finished");
}, 2000);

console.log("queued the timer, moving on");
start
queued the timer, moving on
finished     (~2 seconds later)

The page stays responsive during those two seconds because no JavaScript is running on the stack — it is waiting for the timer callback to be scheduled. We dive deeper into the event loop in Event Loop and Microtasks & Macrotasks.

Putting it all together
  1. Your code is parsed into an AST.

  2. The engine compiles it to bytecode and starts running it on the single call stack.

  3. Functions and objects you create live in the heap, referenced from the stack.

  4. Anything slow — timers, network, user events — is delegated to the host environment, which schedules a callback for later.

  5. When the call stack is empty, the event loop dequeues the next callback and runs it.

  6. Hot code is re-compiled to highly optimised machine code over time.

  7. Unreachable objects are garbage-collected in the background.

Why this matters in practice
Three rules-of-thumb fall out of this model: - Long synchronous work blocks the UI. If a task takes more than ~16ms, break it up or move it to a Web Worker. - The *order* of `setTimeout`, promises and DOM events is governed by the event loop, not by source-code position. Surprising orderings are usually solved by understanding microtasks vs macrotasks. - You don't manage memory, but you can leak it — keep references alive longer than necessary and the garbage collector can't help.