HTML5 game publishing
Why Your Web Game Lags on Phones: Pixel Ratio, Shadows and Draw Calls
A game that runs at 60 FPS on a laptop and stutters on a mid-range phone usually has one of four causes. How to measure, and the fixes that pay off first.
The pattern is familiar. The game is smooth on the developer's laptop, acceptable on a recent flagship phone and unplayable on the mid-range Android phone that most of your players actually own. The cause is rarely one big mistake. It is usually a default setting that was fine on a desktop and expensive on a phone.
Measure frame time before changing anything
Frames per second hides the useful number. A frame budget is 16.7 ms for 60 FPS and 33.3 ms for 30 FPS, so what matters is how long each frame takes and whether that time goes to JavaScript or to drawing. Connect a real phone to your computer and profile it: Chrome's remote debugging at chrome://inspect for Android, and Safari's Web Inspector for an iPhone. Record a few seconds of normal play in the Performance panel and look at one frame.
- If the main thread is busy for most of the frame, the problem is your code: physics, pathfinding, allocations, or a heavy update loop.
- If the main thread finishes early but frames still arrive late, the GPU is the bottleneck: too many pixels, too much shading, or too many draw calls.
- If frames are fine for a minute and then degrade, suspect thermal throttling and check whether the phone is getting hot. Our <a href="https://playkovo.com/blog/why-phone-gets-hot-playing-browser-games">player-side explanation</a> covers what players see.
Test on a phone that is two or three years old and not a flagship. If it is smooth there, it will be smooth nearly everywhere.
Pixel ratio is the biggest hidden multiplier
Phones report a device pixel ratio of 2 or 3. A canvas sized to the window and then multiplied by that ratio holds far more pixels than the player can see a benefit from. A 412 by 915 CSS pixel screen is about 377,000 pixels at a ratio of 1, about 1.5 million at 2 and about 3.4 million at 3. Going from 3 to 2 removes more than half of the pixels the GPU has to fill, and most people cannot see the difference in motion.
In Three.js the setting is renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)). Other engines expose the same idea under names like resolution, scale or render scale. Find it, cap it, and consider a lower cap, such as 1.5, for fast-moving 3D games. UI text drawn on a separate layer can stay sharp at the full ratio.
Shadows cost more than they look
A real-time shadow means the scene is drawn an extra time from the light's point of view into a shadow map, and that map is then sampled for every lit pixel. On a phone this is often the first thing to remove. The options, from most to least effective:
- Turn real-time shadows off and use a simple blob or a darkened circle under characters. In a fast game, nobody checks the shadow's shape.
- Bake lighting and shadows for static scenery into textures in your 3D tool, so nothing is calculated at runtime.
- If you keep real-time shadows, use one shadow-casting light, a small map such as 1024 by 1024 and a tight shadow camera that covers only the play area.
- Skip soft-shadow filtering on phones. The smoothing is extra sampling for every pixel.
Antialiasing and post-processing
Multisample antialiasing multiplies memory and fill work. At a pixel ratio of 2 or more the screen is already dense enough that jagged edges are hard to see, so disabling it on high-density screens is a good trade. Post-processing such as bloom, depth of field and color grading each renders the whole frame again. Treat every pass as a feature that must earn its cost, and drop all of them on weak hardware.
Draw calls and overdraw
Every separate mesh and material is a draw call, and phones handle fewer of them per frame than desktops. Merge static scenery into a single mesh, share materials where possible and use instancing for repeated objects such as trees, coins or crowds. Overdraw is the other quiet cost: stacked transparent layers, large particle sprites and full-screen glow effects make the GPU paint the same pixel several times. Replace a few big overlapping particles with fewer, smaller ones.
Do not forget the JavaScript side
Not every slow game is a graphics problem. Frame-time spikes that arrive every few seconds usually come from garbage collection after code that creates many short-lived objects each frame. Reuse arrays and vectors instead of allocating new ones, and pool bullets, particles and enemies. We go into this in our guide on frame budget, garbage collection and texture memory.
Adapt quality automatically
You cannot test every phone, so let the game adjust. Measure the average frame time over about two seconds. If it stays above roughly 20 ms, lower the render scale by a step such as 0.25, down to a floor of 1. If it stays comfortably under budget for longer, you may step it back up. Do the adjustment gradually and avoid changing it every few frames, or the picture will visibly pulse. A game that quietly lowers its resolution is much better than one that stutters.
A sensible order of work
- Cap the pixel ratio at 2 and re-measure.
- Turn off real-time shadows or reduce them to one small map.
- Disable antialiasing on high-density screens and remove post-processing.
- Merge meshes and instance repeated objects.
- Remove per-frame allocations.
- Add automatic render-scale adjustment as the safety net.
Do these on a real phone and keep a before-and-after recording of the Performance panel. When you submit the game to a portal, mention the devices you tested on. Reviewers who see that kind of testing note are more likely to trust the rest of your submission. Our playtesting guide describes the rest of that checklist.


