DEV Community

Cover image for Your Last Min JS Revision — Part 1: The Runtime
Dhanush S Gowda
Dhanush S Gowda

Posted on • Originally published at dhanushsgowda.hashnode.dev

Your Last Min JS Revision — Part 1: The Runtime

Table of Contents

  1. How JS Goes From Source → Execution

  2. Single-Threaded Foundation

  3. Execution Context & The Two-Phase Model

  4. The Call Stack

  5. Hoisting & The Temporal Dead Zone

  6. The Host Environment — Where JS Stops

  7. Event Loop Mechanics

  8. Priority Model — Microtasks vs Macrotasks

  9. Async/Await — Continuations Under The Hood

  10. Summary — The Event Loop Decision Model


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:

  1. Parser — Converts source code into an AST (Abstract Syntax Tree). This is where syntax errors are caught.

  2. Interpreter — Walks the AST and produces bytecode. Execution starts immediately on bytecode.

  3. Profiler — Watches for "hot" (frequently executed) code paths during execution.

  4. JIT Compiler — Compiles hot bytecode paths into optimized machine code for faster execution.

  5. 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");
Enter fullscreen mode Exit fullscreen mode

Output:

A
B
C
Enter fullscreen mode Exit fullscreen mode

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 declarations

  • Code Component (LexicalEnvironment) — execution happens line by line

The Two Phases

1: Memory Creation
2: Code Execution
Enter fullscreen mode Exit fullscreen mode
var x = 10;
function greet() {
  console.log("Hi");
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Stack state at each step:

Why this matters

Because there is only one call stack:

while (true) {}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

No error. x was allocated and initialized to undefined during Phase 1.

Function hoisting

greet(); // works
function greet() {
  console.log("Hi");
}
Enter fullscreen mode Exit fullscreen mode

The entire function body is hoisted.

let / const hoisting — The Temporal Dead Zone (TDZ)

console.log(a);
let a = 20;
Enter fullscreen mode Exit fullscreen mode

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() {};
Enter fullscreen mode Exit fullscreen mode
sayHi(); // works
function sayHi() {}
Enter fullscreen mode Exit fullscreen mode

Why the difference?

  • function sayHi() {} — full function body hoisted during Phase 1

  • var sayHi = function() {}sayHi is hoisted as undefined (it's a var). The assignment = function() {} happens during Phase 2. Calling undefined() 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)

  • console I/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");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
End
Timeout
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
End
Promise
Timeout
Enter fullscreen mode Exit fullscreen mode

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:

  1. Run ALL microtasks (drain the microtask queue completely)

  2. Run ONE macrotask (dequeue and execute one)

  3. Repeat

Real-world warning: microtask starvation

function loop() {
  Promise.resolve().then(loop);
}
loop();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Output:

1
3
2
Enter fullscreen mode Exit fullscreen mode

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

10. Summary — The Event Loop Decision Model

Final verification

setTimeout(() => console.log("A"));
Promise.resolve().then(() => console.log("B"));
console.log("C");
Enter fullscreen mode Exit fullscreen mode

Output:

C
B
A
Enter fullscreen mode Exit fullscreen mode

Why:

  1. console.log("C") — synchronous, executes immediately

  2. console.log("B") — microtask (Promise then), runs after sync code completes

  3. console.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)