Table of Contents
1. How JS Goes From Source → Execution
JavaScript is not purely interpreted nor purely compiled — it uses Just-In-Time (JIT) compilation. The pipeline varies slightly across engines (V8, SpiderMonkey, JavaScriptCore), but the general flow is:
Step-by-step:
Parser — Converts source code into an AST (Abstract Syntax Tree). This is where syntax errors are caught.
Interpreter — Walks the AST and produces bytecode. Execution starts immediately on bytecode.
Profiler — Watches for "hot" (frequently executed) code paths during execution.
JIT Compiler — Compiles hot bytecode paths into optimized machine code for faster execution.
Deoptimization — If assumptions made by the JIT compiler break (e.g., a variable's type changes), the engine falls back to bytecode.
Key takeaway: Your source code is parsed, converted to bytecode, executed, and selectively optimized at runtime. This is why warm-up iterations matter for performance benchmarks.
2. Single-Threaded Foundation
JavaScript executes:
ONE instruction
at ONE time
on ONE call stack
console.log("A");
console.log("B");
console.log("C");
Output:
A
B
C
No parallelism. No concurrent execution. Code runs sequentially on the main thread. The illusion of concurrency comes from the event loop (covered in Section 7).
3. Execution Context & The Two-Phase Model
Every time code runs, the engine creates an Execution Context. Two types:
| Context | When created |
|---|---|
| Global Execution Context (GEC) | When your script starts |
| Function Execution Context (FEC) | When a function is called |
Each context has two compartments:
Memory Component (
VariableEnvironment) — stores variables and function declarationsCode Component (
LexicalEnvironment) — execution happens line by line
The Two Phases
1: Memory Creation
2: Code Execution
var x = 10;
function greet() {
console.log("Hi");
}
After Phase 1 (Memory Creation):
| Identifier | Value |
|---|---|
x |
undefined |
greet |
Function body (hoisted in full) |
Phase 2 (Code Execution):
var x = 10; // x: undefined → 10
console.log(x); // prints 10
greet(); // creates a new FEC, pushes to call stack
4. The Call Stack
The call stack is a LIFO (Last In, First Out) data structure that tracks which function is executing now.
function one() {
two();
}
function two() {
console.log("Inside two");
}
one();
Stack state at each step:
Why this matters
Because there is only one call stack:
while (true) {}
The browser freezes. The call stack never empties, so the event loop cannot process anything — no rendering, no callbacks, no user input.
5. Hoisting & The Temporal Dead Zone
During Phase 1 (Memory Creation), variable and function declarations are moved to the top of their scope. This is "hoisting".
var hoisting
console.log(x); // undefined
var x = 5;
No error. x was allocated and initialized to undefined during Phase 1.
Function hoisting
greet(); // works
function greet() {
console.log("Hi");
}
The entire function body is hoisted.
let / const hoisting — The Temporal Dead Zone (TDZ)
console.log(a);
let a = 20;
Output: ReferenceError
a does exist in memory — it was allocated during Phase 1. But unlike var, it was not initialized. The gap between allocation and initialization is the Temporal Dead Zone (TDZ).
Comparison table
| Keyword | Allocated in Phase 1? | Initialized? | Default value |
|---|---|---|---|
var |
✅ | ✅ | undefined |
let |
✅ | ❌ (TDZ) | — |
const |
✅ | ❌ (TDZ) | — |
Hidden trap: function expression vs declaration
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {};
sayHi(); // works
function sayHi() {}
Why the difference?
function sayHi() {}— full function body hoisted during Phase 1var sayHi = function() {}—sayHiis hoisted asundefined(it's avar). The assignment= function() {}happens during Phase 2. Callingundefined()produces a TypeError.
6. The Host Environment — Where JS Stops
JavaScript the language does not handle:
Timers (
setTimeout,setInterval)DOM events
Network requests (
fetch,XMLHttpRequest)consoleI/O
These are provided by the host environment — a browser, Node.js, etc.
The engine only executes code and manages the stack. Everything asynchronous is delegated to the host, which pushes callbacks into queues when ready.
7. Event Loop Mechanics
setTimeout — minimum delay, not guaranteed time
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
Output:
Start
End
Timeout
setTimeout(fn, 0) means minimum 0ms before the callback is queued, not "execute instantly". The callback waits in the macrotask queue until the call stack is empty.
8. Priority Model — Microtasks vs Macrotasks
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
Output:
Start
End
Promise
Timeout
Two queues, different priority
| Queue | Contains | Priority |
|---|---|---|
| Microtask Queue | Promise callbacks, queueMicrotask, MutationObserver
|
Higher |
| Macrotask Queue |
setTimeout, setInterval, DOM events, I/O callbacks |
Lower |
The processing rule
After every synchronous operation:
Run ALL microtasks (drain the microtask queue completely)
Run ONE macrotask (dequeue and execute one)
Repeat
Real-world warning: microtask starvation
function loop() {
Promise.resolve().then(loop);
}
loop();
This freezes the browser. Each microtask queues another microtask, so the microtask queue never drains. The event loop never reaches macrotasks or rendering.
9. Async/Await — Continuations Under The Hood
async function test() {
console.log(1);
await Promise.resolve();
console.log(2);
}
test();
console.log(3);
Output:
1
3
2
await suspends the function. Everything after await is wrapped into a microtask continuation.
Conceptually equivalent to:
function test() {
console.log(1);
return Promise.resolve().then(() => {
console.log(2);
});
}
10. Summary — The Event Loop Decision Model
Final verification
setTimeout(() => console.log("A"));
Promise.resolve().then(() => console.log("B"));
console.log("C");
Output:
C
B
A
Why:
console.log("C")— synchronous, executes immediatelyconsole.log("B")— microtask (Promisethen), runs after sync code completesconsole.log("A")— macrotask (setTimeout), runs last after microtasks are drained
One thread, two queues, one loop. No magic — just a determined execution model.



Top comments (0)