I built a browser-based 3D RTS game using Vanilla Three.js. Here is how I handled performance and Instanced Rendering for 60 FPS
I wanted to share the rendering architecture of a project I’ve been working on. It’s a browser-based 3D game called KosmoKode, where players control a space fleet using real JavaScript algorithms.
Since I needed to run untrusted user code, handle WebSockets, and render a space battle simultaneously, performance was my top priority. I decided to skip React Three Fiber (R3F) and heavy frameworks, building the entire visualization in Vanilla Three.js (ES6+). Here is how I structured the scene to maintain a stable 60 FPS.
1. Rendering Hundreds of Objects with InstancedMesh
Space battles require a lot of debris, asteroid fields, and particle effects (like laser beams, engine trails, and explosions). Rendering these individually would kill the CPU with too many draw calls.
- Solution: I heavily utilized
THREE.InstancedMesh. All asteroids of the same type, engine thruster particles, and laser projectiles are grouped into single draw calls. Updating theMatrix4for thousands of instances directly in the animation loop kept the performance incredibly smooth even on integrated laptop GPUs.
2. Decoupling the Render Loop from Game Logic
In a game where users write while loops and heavy pathfinding logic, keeping that code on the main thread would freeze the Three.js requestAnimationFrame loop completely.
- Architecture: All user code and heavy logic run in a separate Web Worker (
codeExecutor.js). The worker calculates the state and sends a compressed payload viapostMessageto the main thread. - The Three.js main thread only does what it does best: interpolates the coordinates and updates the scene graph. The camera, orbit controls, and mesh transformations are completely isolated from the game logic.
3. Asset Management and GLTFLoader
The game currently features 6 distinct ship classes (Fighters, Destroyers, Cruisers, etc.).
- I used
GLTFLoaderto load compressed.gltfmodels. To prevent frame drops during gameplay, all core geometries and materials are pre-loaded and cached during the initial loading screen. When a new ship spawns, the engine just clones the cached scene graph instead of parsing the model again.
4. Reactive UI vs WebGL Canvas
To keep the DOM updates from interfering with WebGL performance, the UI (built with Vanilla JS) sits in a separate layer over the <canvas>. It updates reactively based on WebSocket events, ensuring the Three.js renderer isn't blocked by DOM reflows.
Would love to hear your thoughts on this setup! How do you guys usually handle massive particle systems or asteroid fields in Vanilla Three.js? Any advanced techniques for frustum culling with InstancedMesh I should look into?