Building a game that runs in a browser tab sounds limiting until you realize that WebGL can render 3D worlds at 60fps, Web Audio can mix spatial sound in real-time, and localStorage can save game state without a server. Here's what it actually takes to build a 3D browser game in 2026.
You don't need a game engine. A modern browser game can be built with:
Every game starts with a loop. In the browser, that's requestAnimationFrame:
let lastTime = 0;
function gameLoop(timestamp) {
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
update(dt); // game logic
render(); // draw frame
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
The dt (delta time) parameter is critical โ it ensures your game runs at the same speed regardless of frame rate. A 30fps phone and a 144fps desktop should see the same gameplay speed.
Three.js makes the basics easy:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, w/h, 0.1, 500);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(w, h);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
From here, you add meshes (your game objects), lights (directional for sun, ambient for fill), and materials (standard PBR for realistic surfaces or toon for stylized looks).
Browser games need to handle three input sources:
keydown/keyup events, stored in a key state map so you can check multiple keys per frame.touchstart/touchmove/touchend. Calculate angle and magnitude from center point.navigator.getGamepads().The key insight: never tie game logic to input events directly. Update a state object in event handlers, then read that state in your game loop. This decouples rendering from input and prevents dropped inputs on slow frames.
Browser games save to localStorage. The trick is being smart about what you save:
visibilitychange (tab hidden/closed).The browser is not forgiving about performance. You don't get the luxury of a native game engine's optimization pipeline. Here's what matters:
Every unique material + geometry combination is a draw call. 100 draw calls is fine. 1,000 will stutter. Merge static geometry with BufferGeometryUtils.mergeGeometries() and use instanced rendering for repeated objects (crops, tiles, particles).
Objects far from the camera don't need full geometry. Three.js has built-in LOD support โ swap high-poly meshes for simpler ones based on distance.
Never create and destroy objects in the game loop. Pre-allocate a pool of particles, projectiles, or effects and reuse them. Garbage collection pauses are the #1 cause of frame drops in browser games.
Combine multiple small textures into one large atlas. Fewer texture binds = fewer draw calls = smoother performance. Tools like TexturePacker automate this.
If you're building a browser game in 2026 and it doesn't work on mobile, you're leaving 60%+ of your audience behind. Key considerations:
vmin units so they scale correctly.env(safe-area-inset-*) to keep UI away from system chrome.navigator.maxTouchPoints and reduce shadow quality, particle count, and draw distance.touch-action: none to your canvas and user-scalable=no to the viewport meta tag.Sound makes or breaks immersion. The Web Audio API is powerful but quirky:
After 800+ features and 1,200+ development passes, here are the hard-won lessons:
FarmHeart uses every technique in this article. Open it, poke around the source, and see how a production browser game works.
Play FarmHeart