THN Interview Prep

JS Execution Mechanics: SDE-3 Reference

This section details how JavaScript executes async tasks, compiles code to native machine assembly, and interfaces with hardware-level memory buffers.


7. Event Loop and Message Queue

The Event Loop is the concurrency coordinator in single-threaded JavaScript runtimes. In browsers, it is defined by the HTML Living Standard, while in Node.js, it is driven by libuv.

Loading diagram…

Macrotasks vs. Microtasks

The runtime manages two separate queues for task execution:

  1. Macrotasks (Tasks): Operations scheduled by host APIs.
    • Examples: setTimeout, setInterval, setImmediate (Node), network I/O, user input events, message channel callbacks.
  2. Microtasks: Operations scheduled for immediate execution right after the currently executing JavaScript code finishes and before the call stack is emptied.
    • Examples: Promise.then/catch/finally handlers, MutationObserver callbacks, queueMicrotask(), and process.nextTick (Node.js - though nextTick executes before other microtasks in its own priority queue).

The Execution Loop Algorithm

At every tick of the Event Loop:

  1. Pop and execute the oldest task from the Macrotask queue.
  2. Run the Microtask checkpoint:
    • Loop through the Microtask queue and execute tasks until the queue is completely empty.
    • Note: If a microtask schedules another microtask, it is executed immediately within the same checkpoint. This can block the event loop infinitely if recursively queued.
  3. Check if the screen requires a repaint (typically every 16.7ms for 60Hz).
    • If yes, execute requestAnimationFrame callbacks, recalculate styles, calculate layouts, and paint the frame.
  4. Go back to Step 1.
console.log("Start");

setTimeout(() => console.log("Timeout 1 (Macrotask)"), 0);

Promise.resolve()
  .then(() => {
    console.log("Promise 1 (Microtask)");
    queueMicrotask(() => console.log("Nested Microtask"));
  })
  .then(() => console.log("Promise 2 (Microtask)"));

console.log("End");

// Output Order:
// 1. Start
// 2. End
// 3. Promise 1 (Microtask)
// 4. Promise 2 (Microtask)
// 5. Nested Microtask
// 6. Timeout 1 (Macrotask)

8. IIFE, Modules & Namespaces

As JavaScript applications grew, isolating scopes and managing dependencies became a primary architectural challenge.

1. IIFE (Immediately Invoked Function Expression)

Historical encapsulation pattern that leverages function scope to create a private sandbox.

const counterModule = (() => {
  let count = 0; // Private state
  return {
    increment: () => ++count,
    getVal: () => count
  };
})();

2. CommonJS (CJS)

  • Design: Synchronous and dynamic loading.
  • Syntax: require() and module.exports.
  • Mechanics: When require() is called, the file is read, wrapped in a function, executed synchronously, and the exports are cached. Modifying exported values copies them because CJS uses value copying rather than bindings.

3. ES Modules (ESM)

  • Design: Asynchronous and static loading.
  • Syntax: import and export.
  • Mechanics: ESM goes through three phases before executing:
    1. Construction: Parse files, resolve imports, fetch module records.
    2. Instantiation: Link exports and imports in memory. ESM uses Live Bindings — imports point to the exact memory location of the export, meaning if the exporting module updates the variable, the importer sees the change.
    3. Evaluation: Execute the code to assign values to the linked locations.
// Live Bindings Demonstration (ESM vs CJS)
// ESM Export:
export let count = 0;
export function increment() { count++; }

// ESM Import:
import { count, increment } from './module.js';
console.log(count); // 0
increment();
console.log(count); // 1 (automatically reflects export state change)

9. JavaScript Engines (Focus: V8)

Runtimes like V8 compile human-readable JavaScript code directly into optimized machine instructions.

Loading diagram…

1. Compilation Pipeline

  1. Parser: Lexical analysis transforms source text into tokens, which are parsed into an Abstract Syntax Tree (AST).
  2. Ignition Interpreter: Converts the AST into bytecode. It collects execution profiles (profiling feedback) during execution.
  3. TurboFan JIT Compiler: If a function becomes "hot" (run frequently), TurboFan compiles the bytecode into highly optimized native machine assembly code, using the profiling feedback gathered by Ignition.
  4. Deoptimization (Bailout): If a dynamic type change occurs that invalidates TurboFan's assumptions, the engine discards the optimized code and falls back (bails out) to the interpreter.

2. V8 Memory Optimizations: Hidden Classes (Shapes)

In JS, objects are dynamic and properties can be added on the fly. To avoid looking up offsets in hash tables, V8 uses Hidden Classes (also called Shapes or Maps):

  • If two objects share the same properties in the same order, they share the same Hidden Class.
  • Property access is compiled to a fixed memory offset based on the Hidden Class, mimicking struct layout in compiled languages.
function Point(x, y) {
  this.x = x;
  this.y = y;
}

const p1 = new Point(1, 2); // Hidden Class A
const p2 = new Point(3, 4); // Hidden Class A (Shares same class)

p1.z = 5; // Hidden Class A transitions to Hidden Class B
// p2 still references Hidden Class A. Accessing properties on p1 now requires a transition lookup.

[!TIP] SDE-3 Guideline: Initialize all object properties inside constructors. Adding properties dynamically later causes hidden class transitions, forcing TurboFan to trigger bailouts and fallback to slow property dictionary lookups.


10. setTimeout, setInterval & requestAnimationFrame

1. setTimeout & setInterval Timing Precision

Runtimes do not guarantee that setTimeout runs exactly at the specified millisecond. The delay parameter specifies the minimum time before the task is appended to the Macrotask Queue. If the Call Stack is busy executing sync code, the timer handler must wait.

  • Nesting Clamp: The HTML5 spec forces a minimum delay of 4ms once a timer nesting level exceeds 5 calls.
let start = Date.now();
setTimeout(() => {
  console.log(`Fired after: ${Date.now() - start}ms`); 
}, 10);

// Block the stack for 100ms
while (Date.now() - start < 100) {}
// Even though the timer is ready, it will print: "Fired after: ~100ms"

2. requestAnimationFrame (rAF)

requestAnimationFrame(callback) runs on the UI thread and is synchronized with the browser's refresh rate (usually 60Hz or 120Hz).

  • Execution Position: It fires immediately before the next repaint and layout phase, ensuring smooth visual updates.
  • Page Hiding: When the tab is in the background, rAF pauses, saving CPU and battery. In contrast, setTimeout/setInterval are throttled (clamped to e.g. 1000ms) but continue firing.

11. Bitwise Operators, Typed Arrays & Array Buffers

While standard JS numbers are IEEE 754 floats, binary arrays and bitwise operations require memory-mapped byte access.

1. Bitwise Coercion

When executing bitwise operators (e.g. &, |, ^, ~, <<, >>), the engine converts 64-bit floating-point numbers into 32-bit signed integers, performs the operation, and then converts the result back to a float.

// Truncate decimals using bitwise OR 0
console.log(12.34 | 0); // 12

2. ArrayBuffer & TypedArrays

  • ArrayBuffer: Represents a raw, fixed-length allocation of binary data in memory. You cannot manipulate its bytes directly.
  • TypedArray Views: Wraps an ArrayBuffer with structured views (e.g. Int8Array, Uint8Array, Float64Array) to write/read binary segments directly.
  • DataView: A flexible interface to read/write arbitrary binary formats with custom endianness inside the buffer.
// Allocate 16 bytes of memory
const buffer = new ArrayBuffer(16);

// Create a view to write floats (each Float64 takes 8 bytes)
const floatView = new Float64Array(buffer);
floatView[0] = 3.14159;

// Read the raw binary bytes as 8-bit unsigned integers
const byteView = new Uint8Array(buffer);
console.log(byteView.slice(0, 8)); // Raw binary representations of 3.14159

3. SharedArrayBuffer & Atomics

For multi-threaded environments (using Web Workers), SharedArrayBuffer allows multiple threads to read and write to the same memory segment without copying. To avoid race conditions, the Atomics API provides thread-safe operations (e.g. Atomics.add(), Atomics.load(), Atomics.wait()).

// Thread-safe atomic increment
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);

Atomics.store(sharedArray, 0, 42);
Atomics.add(sharedArray, 0, 1); // Safely increments 42 to 43 across threads

Mark this page when you finish learning it.

Spotted something unclear or wrong on this page?

On this page