Skip to content

Conversation

PoseidonEnergy
Copy link

@PoseidonEnergy PoseidonEnergy commented Jul 26, 2025

This is a draft PR!

Completion status:

✅ rigid_body
✅ toi
❌ narrow_phase
❌ character_controller
❌ ray_cast_vehicle_controller
✅ collider
❌ multibody_joint
❌ impulse_joint
❌ contact
❌ ray
❌ point
❌ pid_controller
❌ shape


This PR is a spiritual continuation of #25 , except we are also now refactoring the Rust side of things, so it is possible to allocate 0 JS objects per call as opposed to 1.


Currently, every time rigidBody.translation() is called, 2 objects are created in JavaScript that have to be garbage collected. This goes for rigidBody.rotation() as well and all the other methods that return objects to JavaScript.

Here is the translation() method:

public translation(): Vector {
let res = this.rawSet.rbTranslation(this.handle);
return VectorOps.fromRaw(res);
}

  1. The line this.rawSet.rbTranslation(this.handle) creates a new RawVector object.
  2. The line VectorOps.fromRaw(res) creates a new Rapier.Vector3 object by essentially cloning the first RawVector object.

In any 3D world you use Rapier.js with, you will need to call translation() and rotation() for each rigid body on every frame in order to synchronize your rendered objects (e.g. THREE.js meshes) to Rapier.js.

So, in a hypothetical world with 1,000 rigid bodies running at 60fps, this is equivalent to 60,000 calls to translation() and 60,000 calls to rotation() per second. Furthermore, since these methods internally create 2 objects each, this is equivalent to 60,000 * 2 * 2 = 240,000 objects/second that need to be garbage collected, and this is ONLY if you call translation() and rotation() once for each rigid body per frame. If you call the methods multiple times for each rigid body per frame, the object creation can easily reach millions per second.

In this pull request, I have changed the implementation for these methods to give the user the option of providing their own pre-allocated "target" object, into which x/y/z values will be copied. I have also changed the Rust counterparts to these methods to NOT return RawVector objects, but rather to take a scratchBuffer object in the form of a Float32Array so that Rust values can be copied there for JavaScript to read from.

RigidBody.translation() now looks like this:

RigidBody (rigid_body.ts)

/**
 * The world-space translation of this rigid-body.
 *
 * @param {Vector?} target - The object to be populated. If provided,
 * the function returns this object instead of creating a new one.
 */
public translation(target?: Vector): Vector {
    this.rawSet.rbTranslation(this.handle, this.scratchBuffer);
    return VectorOps.fromBuffer(this.scratchBuffer, target);
}

VectorOps (math.ts)

public static fromBuffer(buffer: Float32Array, target?: Vector): Vector {
    if (!buffer) return null;

    target ??= VectorOps.zeros();
    target.x = buffer[0];
    target.y = buffer[1];
    target.z = buffer[2];
    return target;
}

RawRigidBodySet (rigid_body.rs)

/// The world-space translation of this rigid-body.
///
/// # Parameters
/// - `scratchBuffer`: The array to be populated.
#[cfg(feature = "dim3")]
pub fn rbTranslation(&self, handle: FlatHandle, scratchBuffer: &js_sys::Float32Array) {
    self.map(handle, |rb| {
        let u = rb.position().translation.vector;
        scratchBuffer.set_index(0, u.x);
        scratchBuffer.set_index(1, u.y);
        scratchBuffer.set_index(2, u.z);
    });
}

The user can utilize this new API like this. In the following example, only one object is allocated, even though we called translation() five times:

const translation = { x: 0, y: 0, z: 0 };
world.step();
console.log(rigidBody.translation(translation));
world.step();
console.log(rigidBody.translation(translation));
world.step();
console.log(rigidBody.translation(translation));
world.step();
console.log(rigidBody.translation(translation));
world.step();
console.log(rigidBody.translation(translation));

Performance Study

Here are the performance gains for 1,000 rigid bodies simulated at 60 FPS with the new API:

  • Chrome v138 - old: 52.8ms, new: 14.1ms (-38.7ms)
  • Firefox Nightly v142.0a1 - old: 173ms, new: 7ms (-166ms)

Anecdotally, I have also experienced a great reduction in GC jank in Firefox especially, aside from the FPS gain.

Run the benchmark yourself here: https://jsfiddle.net/qyg058wd

image

@PoseidonEnergy PoseidonEnergy changed the title [Draft] Mitigate JavaScript Object Allocation [Draft] Mitigate JavaScript Object Allocation for Performance Gain Jul 28, 2025
@PoseidonEnergy
Copy link
Author

Just to give an update--I am continuing work on this PR this weekend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant