Ammo.js Memory Cleanup on Vue Component Unmount

This article explains how to properly manage and release memory allocated by the Ammo.js physics engine when a Vue.js component unmounts. Since Ammo.js is a WebAssembly (Wasm) port of the C++ Bullet Physics library, JavaScript’s native garbage collector cannot automatically free its memory, making manual cleanup essential to prevent severe memory leaks in single-page applications.

The Challenge with Ammo.js and WebAssembly

Because Ammo.js runs in a WebAssembly context, objects created via the new Ammo.bt* constructor are allocated on the Emscripten heap. When a Vue component is unmounted and destroyed, the JavaScript references to these objects are lost, but the allocated memory on the Wasm heap remains. To prevent memory leaks, you must explicitly free these objects using Ammo.destroy() within Vue’s onBeforeUnmount or onUnmounted lifecycle hooks.

Step-by-Step Cleanup Process

To completely clean up Ammo.js, you must dismantle the physics world from the inside out.

1. Remove and Destroy Rigid Bodies

First, iterate through all active rigid bodies in your scene. You must remove them from the physics world, destroy their motion states, destroy their collision shapes, and finally destroy the rigid bodies themselves.

// Example array tracking your active rigid bodies
const rigidBodies = []; 

function cleanupBodies(physicsWorld) {
  rigidBodies.forEach((body) => {
    // Remove from the physics world
    physicsWorld.removeRigidBody(body);

    // Destroy motion state
    const motionState = body.getMotionState();
    if (motionState) {
      Ammo.destroy(motionState);
    }

    // Destroy collision shape
    const shape = body.getCollisionShape();
    if (shape) {
      Ammo.destroy(shape);
    }

    // Destroy the rigid body itself
    Ammo.destroy(body);
  });

  // Clear the array reference
  rigidBodies.length = 0;
}

2. Destroy the Physics World and Core Setup Objects

Once the bodies are removed, you must destroy the infrastructure of the physics world in the reverse order of their creation.

function cleanupPhysicsWorld(worldComponents) {
  const {
    dynamicsWorld,
    solver,
    overlappingPairCache,
    dispatcher,
    collisionConfiguration
  } = worldComponents;

  // Destroy the world
  if (dynamicsWorld) Ammo.destroy(dynamicsWorld);

  // Destroy world infrastructure
  if (solver) Ammo.destroy(solver);
  if (overlappingPairCache) Ammo.destroy(overlappingPairCache);
  if (dispatcher) Ammo.destroy(dispatcher);
  if (collisionConfiguration) Ammo.destroy(collisionConfiguration);
}

3. Implement the Vue Lifecycle Hook

In your Vue component, integrate these cleanup routines inside the onBeforeUnmount hook (for Vue 3 Composition API) to ensure memory is freed before the component is destroyed.

<script setup>
import { onBeforeUnmount, onMounted } from 'vue';

// Keep track of Ammo objects in your component's scope
let physicsWorld;
let solver;
let overlappingPairCache;
let dispatcher;
let collisionConfiguration;
const rigidBodies = [];

onMounted(() => {
  // Initialize your Ammo.js world here
});

onBeforeUnmount(() => {
  // 1. Clean up all rigid bodies, shapes, and motion states
  rigidBodies.forEach((body) => {
    if (physicsWorld) {
      physicsWorld.removeRigidBody(body);
    }
    const motionState = body.getMotionState();
    if (motionState) Ammo.destroy(motionState);
    
    const shape = body.getCollisionShape();
    if (shape) Ammo.destroy(shape);

    Ammo.destroy(body);
  });
  rigidBodies.length = 0;

  // 2. Clean up auxiliary math vectors created during updates
  // (e.g., Ammo.btVector3, Ammo.btQuaternion instances)
  if (typeof temporaryVector !== 'undefined') {
    Ammo.destroy(temporaryVector);
  }

  // 3. Destroy physics world core structures
  if (physicsWorld) Ammo.destroy(physicsWorld);
  if (solver) Ammo.destroy(solver);
  if (overlappingPairCache) Ammo.destroy(overlappingPairCache);
  if (dispatcher) Ammo.destroy(dispatcher);
  if (collisionConfiguration) Ammo.destroy(collisionConfiguration);

  // 4. Nullify references to trigger JavaScript garbage collection
  physicsWorld = null;
  solver = null;
  overlappingPairCache = null;
  dispatcher = null;
  collisionConfiguration = null;
});
</script>

By explicitly invoking Ammo.destroy() on every instantiated Bullet C++ object, you ensure that WebAssembly heap memory is released back to the system, keeping your Vue application’s memory footprint stable.