· 6 min read ·

V8 Left Sea of Nodes and the Rest of the JS World Never Went There

Source: v8

The V8 team published a detailed retrospective on leaving Sea of Nodes in March 2025, covering nearly three years of migration work to replace TurboFan’s intermediate representation with a new one called Turboshaft. Reading it in isolation, the story is about one engineering team fixing a design that had not aged well. Reading it in context, it is a rare example of a major production compiler completing a full circle: CFG to Sea of Nodes and back to CFG. SpiderMonkey and JavaScriptCore never left. That divergence is worth understanding in detail.

What Sea of Nodes Actually Is

Sea of Nodes was introduced by Cliff Click in a 1995 paper co-authored with Michael Paleczny, “A Simple Graph-Based Intermediate Representation.” The core idea is to collapse data flow and control flow into a single unified graph. In a conventional CFG-based IR, you have explicit basic blocks, a total ordering of operations within each block, and control flow edges between blocks. In Sea of Nodes, there are no basic blocks. Operations are nodes in a graph connected by data edges (value dependencies) and control edges (ordering constraints), and they remain unordered until a separate scheduling pass linearizes the graph before code generation.

The theoretical appeal is real. When operations are not pinned to a position in a block, the optimizer has maximum freedom. A value computation can naturally float to wherever it is cheapest. Loop-invariant code motion, for instance, does not need a special analysis pass to discover that a computation belongs outside a loop; the scheduler can simply place it there when it sees that none of the node’s inputs depend on anything inside the loop.

HotSpot’s C2 compiler, the server JIT for Java, adopted Sea of Nodes from Cliff Click’s work, and it has remained there. For a language like Java with a stronger static type system and without the constant speculation and deoptimization overhead that characterizes a JavaScript JIT, C2’s experience with Sea of Nodes has been largely positive. TurboFan brought the same design to V8 around 2015, replacing Crankshaft, which had used a CFG-based IR called Hydrogen.

The Three Problems That Accumulated

The V8 retrospective is candid about what went wrong, and the problems are structural rather than incidental.

The first is scheduling complexity. Because operations float freely in the graph, a separate scheduling pass is required to assign them to blocks before code generation. This scheduling pass is not a minor detail; it is a substantial piece of logic with its own bug surface. Errors that manifest in the output of the compiler often trace to the interface between the floating IR and the linearized result of scheduling, and debugging them requires holding both representations in mind simultaneously. The mismatch between the conceptual model (the graph) and the operational reality (the linearized instruction sequence) creates persistent cognitive overhead for everyone working on the compiler.

The second problem is effect edges. JavaScript has mutable heap state, side-effecting operations, and observable evaluation order. Sea of Nodes handles this by adding explicit effect edges between operations that cannot be reordered relative to each other, such as a heap read that must come after a bounds check. Once you add enough effect edges to correctly model JavaScript semantics, a significant fraction of the IR’s nodes are no longer truly floating. The graph becomes partially ordered in practice, which partially defeats the benefit of having an unordered representation in the first place. You are carrying the conceptual overhead of Sea of Nodes while recovering less of its theoretical flexibility.

The third problem is tooling and debugging. Visualizing a Sea of Nodes graph in a meaningful way requires specialized tooling. The V8 team built and maintained that tooling, but understanding why the compiler produced a particular output requires tracing through a graph with no inherent linear reading order. CFG-based IRs map more directly onto both the source code structure and the machine code structure, which makes the compiler’s reasoning easier to follow.

What Turboshaft Uses Instead

Turboshaft, V8’s replacement IR, is CFG-based with SSA (static single assignment) form. There are explicit basic blocks. Operations within a block have a total order. Phi nodes appear at block entries to reconcile values that arrive from different predecessor blocks. This is the same structure used by LLVM, by GCC’s GIMPLE representation, and by JavaScriptCore’s B3 backend.

The loop example makes the structural difference concrete. Consider a simple sum loop:

function sumArray(arr) {
  let total = 0;
  for (let i = 0; i < arr.length; i++) {
    total += arr[i];
  }
  return total;
}

In a Sea of Nodes IR, arr.length is a load node whose inputs are the arr value and whatever effect token precedes it. To determine whether this load is loop-invariant, the scheduler must perform a graph traversal through the control region nodes that define the loop structure, verify that none of the load’s transitive inputs are defined inside the loop body’s control region, and only then decide to hoist it. The loop structure is implicit in the control edges.

In a CFG-based IR with SSA, the loop is explicit: a header block with phi nodes for i and total, a body block, a backedge, and an exit block. The compiler can see at a glance that arr.length has no definitions inside the loop body or header (assuming no aliasing mutations) and that hoisting it to the loop preheader is valid. The loop structure is part of the representation, not something that must be reconstructed by traversal.

Where the Other Engines Have Been All Along

SpiderMonkey, Mozilla’s JavaScript engine, uses MIR (Medium-level IR), which is CFG-based. The Warp compiler, which replaced the IonMonkey frontend, preserved this CFG approach throughout. SpiderMonkey never adopted Sea of Nodes.

JavaScriptCore, the engine in WebKit and Safari, uses a tiered architecture with the DFG (Data Flow Graph) JIT handling mid-tier compilation and the FTL tier handling hot code. Since around 2016, FTL has used B3 (Bare Bones Backend) as its IR rather than routing through LLVM directly. B3 is explicitly CFG-based with SSA, described by the JSC team as inspired by LLVM’s design but tuned to the needs of a JavaScript JIT. Like Turboshaft, it has explicit blocks, total ordering within blocks, and phi nodes at block entries.

LLVM and GCC GIMPLE, the dominant general-purpose compiler backends, are both CFG-based with SSA. This is not coincidental. The theoretical foundations of CFG-based SSA IRs are well-established, the algorithms for optimizations over them (dominance computation, loop identification, value numbering) are textbook, and the debugging story is significantly better than for unordered graph IRs.

What the Circle Means

The trajectory for V8 is: Crankshaft (CFG, Hydrogen IR) to TurboFan (Sea of Nodes) to Turboshaft (CFG, SSA). SpiderMonkey stayed at the CFG position throughout. JavaScriptCore moved from its DFG tier to B3, also CFG-based. Every major JavaScript engine is now running on a CFG-based IR for its top tier.

Sea of Nodes remains in production in HotSpot’s C2. It is worth asking why a design that has persisted in a Java JIT has caused enough problems in a JavaScript JIT to justify three years of migration work. The answer is probably the combination of JavaScript’s dynamic semantics, the constant need for speculation and deoptimization paths, and the volume of engineers working on the compiler. Effect edges become pervasive in JavaScript because almost any operation can have side effects depending on runtime types. Deoptimization requires carefully tracked checkpoints. The mismatch between the unordered graph model and the practical reality of all these constraints grows over time, and the debugging cost accumulates with the team.

The V8 retrospective is worth reading not because it shows that Sea of Nodes is wrong in some absolute sense, but because it shows what the design costs under a specific set of conditions. Elegant theory buys you something in the optimizer. It charges you something in the tooling, in the debugging loop, and in the gap between the model your IR implies and the one your problem actually has. For the JavaScript engines, the CFG-based accounting has turned out to be cleaner. The ones that never left had that right from the start.

Was this interesting?