60 FPS in a browser game: smooth scenes without cooking the phone

Semih2 min read

Practical notes on draw calls, shadows, textures and pixel ratio for keeping a Three.js browser game smooth on mid-range phones.

A browser game that flows on desktop can turn into a slideshow on a mid-range phone. And players don’t experience it as “the game is lagging” but as “my phone got hot, so I closed it”. The good news: most of the problems come from a few familiar places.

Measure first, then fix

Optimising by guesswork is like looking for your keys in the dark. In Three.js, the first place to look is renderer.info:

setInterval(() => {
  const { calls, triangles } = renderer.info.render;
  console.log({ calls, triangles });
}, 2000);

If draw calls are in the hundreds, the bottleneck is probably not the GPU but the CPU’s cost of sending work to it.

1. Don’t draw the same object over and over

If every crop, every fence and every tree in the field is its own Mesh, each one is its own draw call. For objects sharing a geometry, InstancedMesh turns hundreds of calls into one. Merging the geometry of static decorations does the same job.

2. Shadows are your most expensive decoration

A real-time shadow means drawing the scene once more from the light’s point of view. Practical rules:

  • Enable shadows on a single directional light only.
  • Keep the shadow map small (1024 is usually enough on mobile).
  • For a sense of contact with the ground, a fake “blob” shadow texture often looks better than the real thing.

3. Cap the pixel ratio

New phones can have a device pixel ratio of 3, which means nine times as many pixels to draw. You burn the whole battery on a difference the eye barely sees.

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

4. Textures: small, compressed, shared

Textures fill both download time and GPU memory. In a low-poly world, most objects can be painted from a single colour palette (an atlas), which cuts both file count and material count.

5. Don’t create objects every frame

Writing new THREE.Vector3() every frame in the game loop makes the garbage collector freeze the game now and then. Create temporary vectors once and reuse them.

6. Don’t draw what’s unseen, don’t compute what nobody waits for

Pause the loop when the tab is in the background and freeze animations for off-screen areas. If the player isn’t looking, the phone doesn’t need to work.

Quick summary

Problem First place to look
Too many draw calls Instancing, geometry merging
Heat on mobile Pixel ratio, shadows
Long loading Texture size and compression
Occasional freezes Object creation in the loop

All of this is something low-poly browser games like Farmie need to think about from day one. If you’re curious about the scene’s foundations, start with Your first field in the browser with Three.js.

More from the rulebook

  1. 2 min read

    Your first field in the browser with Three.js

    How do you set up the first scene of a browser game? A step-by-step start with Three.js for camera, light, a low-poly field and clickable plots.

  2. 3 min read

    WHMCS or WiseCP? 6 questions to ask when a hosting company picks a panel

    What to look at when a company selling hosting and domains chooses between WHMCS and WiseCP. There's no winner, only the right questions.

Chance

Got a project in mind? Let’s open the box: tell me what you want to build and I’ll prepare a proposal with the scope and a roadmap.