Clean Up Matter.js Engine on Component Unmount

Failing to properly dispose of a Matter.js engine when a component unmounts can lead to memory leaks, orphan physics loops, and significant rendering performance issues. This guide details how to completely dismantle and garbage-collect a Matter.js instance by systematically stopping the runner, clearing the world composite, disposing of the renderer, and nullifying references.

1. Stop the Render and Runner Loops

Matter.js operates on an active animation loop typically driven by Matter.Render and Matter.Runner. If the component unmounts without halting these, they will continue to execute in the background via requestAnimationFrame.

// Stop the built-in runner
Matter.Runner.stop(runner);

// Stop the canvas renderer
Matter.Render.stop(render);

If you are using a custom requestAnimationFrame implementation instead of Matter.Runner, ensure you call cancelAnimationFrame(animationFrameId).

2. Clear the World Composite

To allow bodies, constraints, and composite hierarchies to be garbage-collected, clear the active world instance:

// Remove all bodies, constraints, and composites from the world
Matter.Composite.clear(engine.world, false);

Passing false as the second argument ensures that child composites are cleared without keeping references.

3. Clear the Engine

Clear the engine instance itself to remove any cached state, collision pairs, and broadphase detection grids:

Matter.Engine.clear(engine);

4. Remove Canvas and Event Listeners

If the renderer created its own <canvas> element or bound events to the window, these elements and listeners must be removed:

// Remove the canvas element from the DOM
if (render.canvas) {
  render.canvas.remove();
}

// Clear internal renderer references
render.canvas = null;
render.context = null;
render.textures = {};

Additionally, if you bound custom mouse interactions using Matter.Mouse or Matter.MouseConstraint, remove the mouse event listeners:

Matter.Events.off(mouseConstraint, 'startdrag');
Matter.Events.off(mouseConstraint, 'enddrag');

Implementation Example in a Component Lifecycle

Below is a standard implementation of the complete teardown process inside a React useEffect cleanup hook, which directly translates to other component-based frameworks (like Vue's onBeforeUnmount or Svelte's onDestroy):

useEffect(() => {
  const engine = Matter.Engine.create();
  const render = Matter.Render.create({
    element: sceneRef.current,
    engine: engine,
  });
  const runner = Matter.Runner.create();

  Matter.Render.run(render);
  Matter.Runner.run(runner, engine);

  // Component unmount cleanup function
  return () => {
    Matter.Runner.stop(runner);
    Matter.Render.stop(render);
    Matter.Composite.clear(engine.world, false);
    Matter.Engine.clear(engine);

    if (render.canvas) {
      render.canvas.remove();
    }

    render.canvas = null;
    render.context = null;
    render.textures = {};
  };
}, []);

Calling these cleanup methods in order ensures that no hanging animation frames, abandoned physics bodies, or orphaned DOM elements remain in memory when your component leaves the DOM.