Memory Layout and the JavaScript Execution Environment

Understanding JavaScript runtime environment and memory management

By Hank Kim

Memory Layout and the JavaScript Execution Environment

Understanding JavaScript runtime environment and memory management

Memory Layout and the JavaScript Execution Environment

This document ties together classical process memory layout and the way JavaScript runs in browsers and Node.js. It consolidates your notes without dropping any concepts.


1. Classical Memory Layout vs JS Runtime

In systems programming, a process memory layout is often described as: code, data, stack, and heap. JavaScript engines adopt a compatible mental model:

JavaScript runtime structure overview

  • Call Stack (Stack): execution contexts of functions in a LIFO structure.
  • Heap: dynamically allocated, unstructured memory for objects/arrays/functions.

2. Call Stack, Execution Contexts, and Heap

  • Call Stack

    • Holds execution contexts (function call records).
    • On call → push; on return → pop. Deep recursion can cause stack overflow.
    • Locals/parameters and references to heap objects are associated with these frames.
  • Heap

    • Stores reference types: objects, arrays, closures’ captured environments, etc.
    • Accessed indirectly via references held by stack frames or other heap objects.

3. Garbage Collection (GC)

  • Stack frames disappear when functions return; no explicit GC needed.
  • Heap is periodically scanned by the engine’s GC to reclaim objects that are no longer reachable (no live references).
  • Memory leaks happen when references to no-longer-needed objects are retained.

4. “JavaScript is Single-Threaded” — What It Actually Means

Main thread and background thread communication

  • The JS engine (e.g., V8) runs one call stack; it executes your JS code synchronously, one thing at a time.
  • While one frame is executing, no other JS code runs concurrently on that same thread.
  • Heavy CPU work (e.g., image processing, big loops) can block the main thread and freeze the UI.

5. How Asynchrony Works: Runtime Cooperation

JavaScript the language is synchronous; asynchrony is provided by the runtime (browser or Node.js) via background subsystems.

Browser

  • JS Engine (V8, etc.): manages the call stack and heap; does not perform I/O.
  • Web APIs (the runtime’s asynchronous I/O managers):

    • Timers (setTimeout, setInterval)
    • Network (fetch, XHR)
    • DOM events, Geolocation, etc.
    • They run in the browser’s own threads/subsystems (timer thread, network thread, etc.), not on the JS engine thread.
  • Queues

    • Task Queue (a.k.a. “macro-task queue”): e.g., callbacks from setTimeout, message events, etc.
    • Microtask Queue: e.g., Promise.then/catch/finally, MutationObserver. Microtasks run before the next task and before the next paint when the stack becomes empty.
  • Event Loop

    • Continuously checks: if the call stack is empty, it drains the microtask queue, then takes the next task and pushes it onto the stack.

Node.js

  • Uses libuv (C++) to handle asynchronous I/O with a thread pool and OS facilities.
  • File system, sockets, DNS, etc., are managed by libuv; when complete, callbacks are queued and later executed by the JS thread via Node’s event loop.
  • Conceptually mirrors the browser model: single JS thread + background I/O + queues + event loop.

6. End-to-End Flow of an Async Operation

  1. Your code calls an async API (e.g., setTimeout, fetch).
  2. The call is registered with the runtime (Web API/libuv) and the JS function returns, clearing from the call stack.
  3. The background subsystem waits or performs I/O.
  4. When done, it enqueues the completion callback:

    • Promises → microtask queue
    • Timers, message events, etc. → task queue
  5. The event loop runs:

    • If the call stack is empty, it drains microtasks, then takes the next task and executes it by pushing it onto the stack.
  6. During callback execution, if you call more functions, they are pushed on top of the current stack frame as usual.

Only when the call stack is empty can a queued callback move onto the stack.


7. Synchronous vs Asynchronous Code

  • Synchronous: runs to completion before the next statement; blocks subsequent execution.
  • Asynchronous: defers completion via callbacks/promises/async-await; the runtime handles waiting in the background; the event loop schedules completion code later.

This design avoids blocking the main thread for long-latency operations (network, filesystem, DB), preventing UI and server stalls.


8. Callback Queue, Task vs Microtask, and Ordering

Web API handling timer operations

  • Callback queue colloquially refers to the task queue (macro-tasks).
  • Microtasks have higher priority: once the stack is empty, the engine runs all queued microtasks before taking the next macro-task.
  • Examples:

    • setTimeout(fn, 0) → task
    • Promise.resolve().then(fn) → microtask
  • Implication: promise chains can run before timer callbacks scheduled for “now”. Excessive microtasks can starve tasks until the microtask queue empties.

9. Web APIs vs Web Workers

  • Web APIs: asynchronous I/O managers supplied by the browser; they schedule callbacks back to JS via queues. They are not separate JS runtimes and do not run your JS code.
  • Web Workers:

    • A separate JS execution environment created explicitly with new Worker().
    • Own call stack and heap, running on another thread.
    • No DOM access; communicates with the main thread via message passing only:

      • Main → Worker: worker.postMessage(data)
      • Worker → Main: self.postMessage(data)
      • Listen via onmessage.
    • Use case: offload CPU-bound work so the UI thread stays responsive.
  • Not the same thing: Web Workers are not the browser’s I/O managers; they are additional JS threads you create.

10. Relationship Summary

  • JS Engine: single call stack + heap; executes code synchronously.
  • Background I/O (Web APIs/libuv): performs timers, network, file I/O off the JS thread.
  • Queues: completed work is queued (microtasks vs tasks).
  • Event Loop: moves callbacks to the stack when it is empty, enforcing ordering.
  • Web Worker: separate JS runtime for heavy computation; communicates via messages.

11. Practical Consequences

  • Long CPU tasks on the main thread block the call stack; offload to Web Workers when needed.
  • I/O-heavy work should use the runtime’s async facilities (Web APIs/libuv) to avoid blocking.
  • Memory hygiene matters: clear references to allow heap GC; avoid accidental retention that leads to leaks.
  • Understand queue ordering:

    • Promise microtasks can run before timer tasks, even with 0 delay.
    • The event loop only dequeues when the call stack is empty.

12. Recap

JavaScript executes on a single call stack with heap-allocated objects, while asynchrony is provided by the runtime (Web APIs/libuv) that completes work in the background and, via the event loop and queues, schedules callbacks back onto the JS thread; Web Workers are separate JS runtimes for CPU-bound work and communicate through message passing.


Tags: JavaScript