JavaScript Execution: Parsing, the Event Loop, JIT and GC

What actually happens between a <script> tag and a responsive page. V8 defers most function bodies rather than skipping them, building no AST until first call; a long task is not descheduled, it runs to completion because interrupts are only handled at checkpoints the running code reaches; optimized code is thrown away when a guard fails; and a FinalizationRegistry callback is a posted task, not a destructor. Read from V8 15.4 and Chromium 154 source, not from blog-post folklore.

Launch Simulator →

What you'll explore

  1. 01

    Parsing and compilation

    V8 does not fully parse the script you send it. On the first read the parser reaches each function literal and decides eager or lazy; for a lazy function it hands the body to the PreParser, which walks the tokens without building an AST and records just enough to rebuild the function later. The body is fully parsed on first call. So the cost of a byte depends on whether anything calls it — which is why shipping code the first screen never touches is not free but is also not full price.

  2. 02

    The event loop, tasks and microtasks

    Tasks and microtasks are not two priorities of the same queue; they are two different mechanisms. A task is scheduled by the embedder. Microtasks live in a V8 MicrotaskQueue that is drained to empty at a checkpoint — so a promise chain that keeps adding continuations keeps the checkpoint running and never yields. requestAnimationFrame is a third thing again: its callback list is snapshotted before the frame runs, so a callback registered from inside a frame callback is deferred to the next frame.

  3. 03

    Main-thread blocking and long tasks

    The W3C Long Tasks API calls any task over 50 ms a long task; that threshold is a spec definition, not a V8 or Blink constant, and nothing in the engine enforces it. What the engine does have is an interrupt mechanism, and it is cooperative: interrupts are handled only at checkpoints the running code itself arrives at, and the list of interrupt kinds contains nothing resembling "the scheduler wants the thread back". A 500 ms loop is therefore not descheduled and resumed — it runs to the end, and every click, keypress and frame waits behind it.

  4. 04

    Optimization tiers and deoptimization

    V8 starts in the interpreter and tiers up on evidence, not on a timer. Type feedback is collected in a FeedbackVector attached to the function, and the tiering manager consults that vector to pick Maglev or TurboFan. Optimized code is speculative: it contains guards asserting the shapes it was compiled for. When a guard fails the frame is deoptimized back to the interpreter — the reason list names cases like "wrong map", "map changed during operation" and insufficient type feedback. A monomorphic call site that turns polymorphic pays for it twice.

  5. 05

    Memory, GC pauses and weak references

    A major collection takes a safepoint: the mutator is stopped, which is why a long enough atomic pause shows up as a dropped frame. Weak references complicate the mental model rather than simplifying it. When a collection finds an object registered with a FinalizationRegistry, it marks that registry dirty during the collection and then POSTS a cleanup task; the callback therefore runs in a later turn of the event loop, separated from the collection by a task boundary. Treating it as a destructor is a race, not a pattern.

Validated against V8 (parser, Ignition/Maglev/TurboFan tiering, heap) + Chromium Blink script and animation-frame scheduling v8-15.4.80.11 + chromium-154.0.8037.57 — src/parsing/parser.cc and friends. Every simulated behavior cites a real source function, and a server-side correlation engine computes how each layer's state cascades into the next. See the methodology →

This module is part of zerohop Pro — it unlocks alongside the full kata library, progress tracking, and weak-area analysis.