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.