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:
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.
Compilation. A baseline compiler turns the AST into bytecode or quick machine code so execution can start fast.
Execution. The engine runs the compiled code. While it runs, it watches which functions are "hot" — used often.
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.
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
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
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
Your code is parsed into an AST.
The engine compiles it to bytecode and starts running it on the single call stack.
Functions and objects you create live in the heap, referenced from the stack.
Anything slow — timers, network, user events — is delegated to the host environment, which schedules a callback for later.
When the call stack is empty, the event loop dequeues the next callback and runs it.
Hot code is re-compiled to highly optimised machine code over time.
Unreachable objects are garbage-collected in the background.