Building High-Performance 3D Simulation Engines in WebGL & TypeScript
Architectural insights from Startup Empire 3D: maintaining 60 FPS in browser-based spatial simulations.

The Constraint of the 16.6ms Render Frame
To achieve a smooth 60 frames per second in the browser, your entire calculation—including input processing, game state simulation, transform matrix updates, and WebGL draw calls—must complete within 16.6 milliseconds.
When developing Startup Empire 3D, we encountered frame drops caused by typical JavaScript habits: allocating objects inside the animation loop, triggering browser garbage collection pauses, and executing hundreds of individual draw calls.
Instanced Mesh Batching and Object Pooling
Instead of creating individual 3D meshes for hundreds of buildings, workers, and desks, we consolidated visual elements into `THREE.InstancedMesh`. This reduced WebGL draw calls from over 450 to just 6 per frame.
Furthermore, zero memory allocations occur inside `requestAnimationFrame`. All vector math and matrix multiplications operate on pre-allocated object pools.
// Pre-allocated scratch objects to prevent GC pauses
const scratchMatrix = new THREE.Matrix4();
const scratchPosition = new THREE.Vector3();
const scratchQuaternion = new THREE.Quaternion();
const scratchScale = new THREE.Vector3(1, 1, 1);
export function updateBuildingTransforms(instancedMesh: THREE.InstancedMesh, entities: Entity[]): void {
for (let i = 0; i < entities.length; i++) {
scratchPosition.set(entities[i].x, entities[i].y, entities[i].z);
scratchMatrix.compose(scratchPosition, scratchQuaternion, scratchScale);
instancedMesh.setMatrixAt(i, scratchMatrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
}- Never allocate objects or arrays inside requestAnimationFrame.
- InstancedMesh batching reduces draw calls by two orders of magnitude.
- Spatial simulation performance is primarily a memory layout and GC mitigation challenge.