Your browser game runs great on your development machine. Then someone opens it on a 2019 Chromebook and it drops to 12fps. Sound familiar? Here's how to build browser games that run smoothly on everything from budget phones to gaming PCs.
At 60fps, you have 16.67 milliseconds per frame. That's time for input processing, game logic updates, physics simulation, and rendering — everything. On a budget device, the GPU is slow and the CPU is fighting 47 Chrome tabs for time.
| Target FPS | Frame Budget | Realistic After Overhead |
|---|---|---|
| 60fps | 16.67ms | ~12ms (browser uses ~4ms) |
| 30fps | 33.33ms | ~28ms |
| 24fps (minimum playable) | 41.67ms | ~36ms |
Aim for 60fps. Accept 30fps on weak devices. Below 24fps the game feels broken.
Every time the GPU switches materials, textures, or geometry, that's a draw call. Desktop GPUs handle thousands easily. Mobile GPUs struggle past 200.
Fix: Merge and instance.
mergeGeometries().InstancedMesh. One draw call for 500 crop tiles instead of 500 separate calls.JavaScript's garbage collector pauses everything when it runs. In a game, that means frame drops — sometimes for 50-100ms. The fix is simple in concept but hard in practice: don't allocate memory in the game loop.
const _tmpVec = new THREE.Vector3() at module scope, reuse everywhere.Float32Array) for bulk math instead of regular arrays.Overdraw happens when the GPU draws pixels that get covered by something in front of them. Transparent objects are the worst offenders — they can't be depth-sorted early, so the GPU renders them fully even when invisible behind opaque geometry.
The smartest optimization: detect the device and scale quality accordingly.
const isMobile = navigator.maxTouchPoints > 0;
const isLowEnd = navigator.hardwareConcurrency <= 4;
if (isMobile || isLowEnd) {
renderer.setPixelRatio(1); // skip retina
renderer.shadowMap.enabled = false; // shadows are expensive
scene.fog.far = 80; // reduce draw distance
MAX_PARTICLES = 100; // fewer particles
} else {
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
scene.fog.far = 200;
MAX_PARTICLES = 500;
}
You can also dynamically adjust quality: if the average frame time exceeds 20ms over the last 60 frames, automatically reduce shadow quality or particle count. Players would rather have smooth gameplay than pretty effects.
Textures eat VRAM. A single 4096x4096 texture uses 64MB. On a phone with 2GB total VRAM, that's a problem.
If you're using a physics engine (Cannon-es, Rapier, Ammo.js), these rules apply:
A browser game that takes 30 seconds to load is a browser game nobody plays. Target under 5 seconds to first interaction.
Cache-Control: max-age=31536000, immutable on versioned assets. Return visitors should load instantly.You can't optimize what you can't measure:
FarmHeart runs 800+ gameplay features with 3D rendering, weather systems, particle effects, and dynamic lighting on devices ranging from iPhone SE to gaming desktops. The key wasn't any single optimization — it was the discipline of profiling every new feature and keeping the frame budget under control.
Most browser games don't fail because of technical limitations. They fail because developers add features without measuring their cost. Set your frame budget, respect it, and your game will run anywhere.
FarmHeart demonstrates all of these techniques in a production game. Open it on your phone to see adaptive quality in action.
Play FarmHeart