Profiling Vector Memory Leaks in Matter.js

High-frequency physics simulations built with Matter.js often suffer from frame drops and memory bloat caused by excessive allocations of vector objects within the update loop. This article outlines how to identify and profile Matter.Vector allocation spikes using browser developer tools, trace retaining paths, and refactor physics code to maintain a flat memory footprint.

Why Matter.js Vectors Leak

Matter.js relies heavily on 2D vectors for positions, velocities, and forces. Methods such as Vector.create(x, y), Vector.add(), and Vector.sub() return fresh object instances:

// Creates a new object reference every execution
const force = Matter.Vector.create(0, 0.05);
Matter.Body.applyForce(body, body.position, force);

When called repeatedly inside beforeUpdate events or requestAnimationFrame cycles, thousands of short-lived { x, y } objects are allocated every second. If references to these vectors are inadvertently retained in arrays, custom cache structures, or closures, the garbage collector cannot free them, causing a steady climb in heap size. Even without permanent retention, frequent allocations trigger garbage collection (GC) thrashing, causing visible micro-stutters.

Step-by-Step Profiling with Chrome DevTools

To isolate transient or leaking vector objects, use the Chrome DevTools Memory profiler.

1. Capture Allocation Instrumentation on Timeline

  1. Open your application in Google Chrome.
  2. Open DevTools (F12 or Ctrl+Shift+I / Cmd+Option+I) and switch to the Memory panel.
  3. Select Allocation instrumentation on timeline.
  4. Click Start to begin profiling while your Matter.js physics engine is actively stepping.
  5. Let the simulation run for 10–20 seconds to establish a baseline, then perform the actions suspected of causing leaks.
  6. Click the red record button to stop.

2. Isolate Vector Allocations

The timeline displays vertical bars representing memory allocations:

A sustained staircase pattern of blue bars confirms a genuine memory leak. To inspect vectors:

  1. Drag a selection window over a section of the timeline with high blue spikes.
  2. In the Constructor view, filter by Object.
  3. Expand Object and sort by Shallow Size or Distance. Matter.js vectors appear as generic objects containing only x and y numeric properties.
  4. If you have wrapped vector creation in custom classes, search directly for your class name.

3. Trace Retaining Paths

Click on an individual vector instance to populate the Retainers panel at the bottom:

  1. Look for the chain of references keeping the object alive.
  2. Identify whether the vector is held by a custom array (such as an unpruned history of past body positions), an internal Matter.js property (such as body.parts or custom plugin state), or an active closure context (such as an event listener callback created inside a loop).
  3. Check the Distance column: lower numbers indicate objects closer to the root (e.g., window), which are actively preventing garbage collection.

Taking Comparative Heap Snapshots

For persistent leaks:

  1. In the Memory panel, select Heap snapshot.
  2. Take Snapshot 1 immediately after the physics world stabilizes.
  3. Allow the simulation to run for several minutes.
  4. Take Snapshot 2.
  5. Switch the view dropdown from Summary to Comparison, comparing Snapshot 2 against Snapshot 1.
  6. Sort by # Delta. If Object shows a large positive delta matching your frame count multiplied by your bodies, vectors are failing to clean up.

Eliminating Vector Allocations

Once you find the offending code, eliminate the creation of new vector instances.

Mutate Existing Vectors

Modify the properties of an existing vector instead of generating a new one:

// Avoid
const displacement = Matter.Vector.sub(bodyA.position, bodyB.position);

// Preferred: Reuse a dedicated vector object
displacement.x = bodyA.position.x - bodyB.position.x;
displacement.y = bodyA.position.y - bodyB.position.y;

Pre-allocate Static Vectors

For static values like gravity offsets or standard directions, allocate them outside the execution loop:

// Defined once in module or setup scope
const ZERO_FORCE = Object.freeze({ x: 0, y: 0 });
const UPWARD_FORCE = { x: 0, y: -0.05 };

Matter.Events.on(engine, 'beforeUpdate', () => {
    Matter.Body.applyForce(playerBody, playerBody.position, UPWARD_FORCE);
});

Implement a Vector Pool

For complex math requiring intermediary calculations, use an object pool:

const VectorPool = {
    pool: [],
    acquire(x = 0, y = 0) {
        const v = this.pool.pop() || { x: 0, y: 0 };
        v.x = x;
        v.y = y;
        return v;
    },
    release(v) {
        this.pool.push(v);
    }
};

// Usage inside the physics loop
const tempVec = VectorPool.acquire(target.x, target.y);
// ... compute physics ...
VectorPool.release(tempVec);

After implementing these modifications, run another Allocation instrumentation session to verify that vector allocations drop to zero during steady-state simulation.