Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GC jitter: how JS GC breaks predictable performance

This section shows how JavaScript’s garbage collection can negatively impact predictable performance.

The workload

A deeply nested, pointer-heavy graph that tries to emulate some real world application state that is used to, per call:

  • Build the tree.
  • Sum every node’s value via a recursive walk.
  • Push the resulting tree onto a rolling retention buffer.

JavaScript variant

Plain { value, children } objects on the JS heap. The GC owns the graph.

interface JsTreeNode {
    value: number;
    children: JsTreeNode[];
}

function jsBuildTree(depth: number, branching: number, seed: number): JsTreeNode {
    const children: JsTreeNode[] = [];
    if (depth > 0) {
        for (let i = 0; i < branching; i++) {
            const childSeed = ((seed * 31) + i) >>> 0;
            children.push(jsBuildTree(depth - 1, branching, childSeed));
        }
    }
    return { value: seed, children };
}

function jsSumTree(node: JsTreeNode): number {
    let sum = node.value;
    const children = node.children;
    for (let i = 0; i < children.length; i++) {
        sum = (sum + jsSumTree(children[i])) >>> 0;
    }
    return sum;
}

Rust → Wasm variant

TreeNode records linked through Vecs in Wasm linear memory. Memory is freed deterministically in the VecDeque.

#![allow(unused)]
fn main() {
struct TreeNode {
    value: u32,
    children: Vec<TreeNode>,
}

fn build_tree(depth: u32, branching: u32, seed: u32) -> TreeNode {
    let mut children: Vec<TreeNode> = Vec::new();
    if depth > 0 {
        children.reserve_exact(branching as usize);
        for i in 0..branching {
            let child_seed = seed.wrapping_mul(31).wrapping_add(i);
            children.push(build_tree(depth - 1, branching, child_seed));
        }
    }
    TreeNode { value: seed, children }
}

fn sum_tree(node: &TreeNode) -> u32 {
    let mut sum = node.value;
    for child in &node.children {
        sum = sum.wrapping_add(sum_tree(child));
    }
    sum
}
}

What you should see

  • Top chart (per-call work time, ms). The JS half bobs around the same baseline most of the time, but sees regular spikes. The Wasm half stays close to a flat band.
  • Bottom chart (heap MB). The JS half climbs in a sawtooth pattern. A slow ramp occurs as memory fills, then a sharp drop every time a major GC cycle completes. Each drop on the heap chart lines up with a spike on the work-time chart above. The Wasm half stays almost flat.

performance.memory is a Chromium-only API. In Firefox and Safari the heap chart will stay empty.

Analysis

Rust + Wasm do not require garbage collection. Memory is freed deterministically and, unlike JavaScript, do not accumulate into a single moment that can cause a dropped frame.