Optimizing Physics Performance
These techniques apply to every web physics engine, whether you use Rapier, Cannon-es, or Havok. The underlying principles are the same: give the engine less work to do, make each unit of work cheaper, and prevent physics spikes from blocking the renderer.
Reduce Active Body Count
The single most effective optimization is having fewer bodies in the simulation. Physics cost scales with the number of active bodies, roughly linearly for broadphase and solver work. Going from 500 bodies to 250 roughly halves the physics time.
Remove bodies that are no longer needed. Debris from an explosion does not need to persist forever. After fragments settle and the camera moves on, remove them from the world. Projectiles that leave the play area should be removed immediately, not left flying into infinite space consuming broadphase checks.
Use spatial culling to deactivate distant bodies. If the player is in one part of a large level, bodies in distant rooms do not need to simulate. Remove them from the world or switch them to static when the player is far away, and re-add them as dynamic when the player approaches. This requires careful bookkeeping (save their state so they resume correctly) but dramatically reduces the active body count in large levels.
Merge small static bodies into larger compound shapes. A level made of individual 1x1 tile bodies creates thousands of static colliders that bloat the broadphase tree. Instead, merge adjacent solid tiles into larger rectangular bodies. A row of 20 tiles becomes one long box. This reduces the broadphase node count without changing collision behavior.
Object pooling prevents garbage collection spikes from frequent body creation and destruction. Instead of creating new CANNON.Body or Rapier.RigidBody instances for each bullet or particle, maintain a pool of pre-created bodies. When you need one, pull from the pool and reposition it. When it is done, return it to the pool. This avoids the cost of allocating and initializing new physics objects at runtime.
Simplify Collision Shapes
Simpler shapes are faster to test. A sphere-vs-sphere test is one subtraction and one comparison. A convex hull pair requires the iterative GJK algorithm. A triangle mesh test checks every triangle against the other shape. The difference in cost per pair can be 10x or more between a sphere and a convex hull.
Use primitives whenever possible. A character's collision shape should be a capsule, not a detailed mesh of their visual model. A tree should be a cylinder for the trunk, not a convex hull of every branch. A crate should be a box even if the visual model has beveled edges and surface detail. Players do not notice that the collision shape is slightly simpler than the visual mesh.
Avoid triangle mesh (trimesh) colliders for anything except static level geometry. Trimesh collision is the most expensive shape type because it tests individual triangles. Never use a trimesh for a dynamic body. If a dynamic object has a complex shape, use convex decomposition to break it into a small number of convex parts and create a compound shape from those parts.
Convex decomposition tools (like V-HACD) split a concave mesh into multiple convex hulls. The result is a compound shape with, say, 5 convex hulls instead of a 500-triangle mesh. Each hull is tested with GJK, which is much faster than testing 500 triangles. Most engines support compound shapes natively, and some (like Rapier) can generate convex decompositions from mesh data at runtime.
Reduce convex hull vertex counts. A convex hull with 50 vertices is much cheaper than one with 500. Most physics engines let you specify a maximum vertex count when generating a hull. For game-scale objects, 16 to 32 vertices is usually sufficient for a convincing collision shape.
Tune the Broadphase
The broadphase eliminates pairs that cannot collide before the expensive narrowphase runs. Choosing the right broadphase algorithm and configuring collision groups correctly can cut broadphase time by half or more.
For Rapier, the built-in dynamic BVH is already well optimized with SIMD-accelerated traversals. There is little to tune, but you can improve it by minimizing the number of very large colliders (which create oversized bounding boxes that overlap many tree nodes).
For Cannon-es, switch from NaiveBroadphase (which tests all pairs) to SAPBroadphase for any scene with more than about 30 bodies. SAPBroadphase maintains a sorted list along one axis and only tests neighbors in the sorted order. For scenes where objects are spread evenly in all directions (not clustered along one axis), consider a grid-based broadphase if available, or accept that SAP will be somewhat less efficient.
Collision groups are the broadphase's most powerful optimization. Assign bodies to groups by category: players, enemies, projectiles, environment, pickups, sensors. Set filter masks so that categories that never interact are never tested. Player projectiles should not test against the player. Pickups should not test against enemies. Environment bodies should not test against each other. Every skipped pair is a pair the narrowphase never has to evaluate.
In practice, well-configured collision groups can reduce broadphase candidate pairs by 50% to 80%, which translates directly into less narrowphase work and faster physics steps.
Configure Sleep and Deactivation
Sleeping is a built-in optimization where bodies that have been nearly stationary for a threshold period stop being processed by the solver. A sleeping body takes almost zero CPU time. It wakes up automatically when another body collides with it or when your code applies a force or impulse.
Enable sleep globally: in Cannon-es, set world.allowSleep = true. In Rapier, bodies sleep by default. In Havok via Babylon.js, sleep is managed automatically by the engine. Once enabled, tune the thresholds. The sleep speed limit is the velocity below which a body is considered stationary (default is usually around 0.1 m/s). The sleep time limit is how long the body must be stationary before sleeping (default is usually around 1 second).
Lower speed limits make bodies sleep more aggressively, which saves more CPU but can cause visible snapping if a slowly moving body suddenly freezes. Higher speed limits keep bodies awake longer, which looks smoother but costs more CPU. For most games, the defaults work well. Adjust only if you see bodies freezing visibly (lower the time limit or raise the speed limit) or if too many bodies stay awake unnecessarily (lower the speed limit).
Be careful about waking bodies unnecessarily. If your game logic touches a body's position or applies a tiny force every frame (like a health regeneration effect), it will prevent the body from ever sleeping. Only interact with bodies that actually need to move.
Offload Physics to a Web Worker
Running physics in a Web Worker prevents simulation spikes from blocking the main thread, where rendering and input handling happen. Even if the physics step occasionally takes 10ms, the main thread continues rendering at full speed because the work happens in a separate thread.
The architecture is straightforward. The main thread handles rendering, input, and UI. A dedicated Web Worker hosts the physics world. Each frame, the main thread sends input commands (forces, impulses, body creation/removal) to the worker via postMessage. The worker steps the physics world and sends back an array of body transforms (position and rotation for each body). The main thread reads the transforms and updates the visual meshes.
For optimal data transfer, use SharedArrayBuffer to share a typed array between threads. The worker writes body transforms into the shared buffer after each step, and the main thread reads from it before each render. This avoids the overhead of structured cloning that postMessage normally requires. A Float32Array with 7 floats per body (3 for position, 4 for quaternion) is compact and fast to read.
Rapier is ideal for worker-based physics because its WASM module initializes cleanly in a worker context with no DOM dependencies. Cannon-es also works in workers since it is pure JavaScript. Havok inside Babylon.js is more tightly coupled to the scene and is harder to move to a worker, but Babylon.js handles its own internal threading optimizations.
The trade-off is latency. The visual meshes display the physics state from one frame ago (or one step ago if you double-buffer). For most games this single-frame delay is imperceptible, but for games that require instant, frame-exact collision feedback (like competitive fighting games), the latency may be unacceptable. Test with your specific gameplay before committing to a worker architecture.
Profile and Measure
Never optimize blindly. Measure the actual physics step time before and after each change. Use performance.now() to time the world.step() call: const start = performance.now(); world.step(dt); const elapsed = performance.now() - start. Log or graph this value to see your physics frame budget usage.
Browser devtools (Chrome Performance tab, Firefox Profiler) show where CPU time goes within the physics step. You can see broadphase, narrowphase, and solver time if the engine uses named functions (Rapier's WASM calls appear as wasm functions, so JavaScript-level profiling is more useful with Cannon-es).
Track the number of active (non-sleeping) bodies, the number of contact pairs, and the number of constraints. These three numbers predict physics cost. If active body count spikes when debris spawns, that is your bottleneck. If contact pair count is high relative to body count, your collision groups are not filtering effectively.
Set a frame budget target. At 60 fps, you have 16.67ms total. Rendering typically takes 6 to 10ms. Game logic takes 1 to 3ms. That leaves 3 to 8ms for physics. If your physics step consistently exceeds that budget, apply the optimizations in this guide in order of impact: reduce body count first, simplify shapes second, tune broadphase and sleep third, and move to a worker if the main thread budget is still tight.
Physics performance comes down to giving the engine less work: fewer bodies, simpler shapes, smarter broadphase filtering, and aggressive sleep management. When that is not enough, move the physics world to a Web Worker so it cannot block rendering. Always profile before and after changes to verify they actually help.