A couple of months back, we pulled the build logs for a small tower-defense game one of our teams had been polishing for weeks. On the test devices, a Pixel 7 and a Galaxy S23, it sat at a rock-solid 60fps. Then a tester ran it on a Redmi 9A. 19fps. The tutorial popups overlapped because the animation couldn’t keep up with the trigger timing. It wasn’t broken code. It was a phone with 2GB of RAM trying to do what a flagship does without complaint.

That gap is more common than most people realize, and it’s the kind of thing every mobile game development company eventually runs into head-on. The good news is that fixing it doesn’t mean making the game look worse. It mostly comes down to figuring out where the performance budget actually needs to go, and not wasting it anywhere else.

Here’s what that looks like in practice.

“Low-End” Isn’t What People Think It Is

A lot of people hear “low-end Android” and picture some ancient device from 2017. That’s not really the issue anymore. Most of the phones we’re talking about were released in the last two or three years. Android Go Edition, 2 to 3GB of RAM, a Snapdragon 4-series or MediaTek Helio G-series chip, an Adreno 5xx or Mali-G5x GPU. These phones sell in huge numbers across Southeast Asia, Latin America, the Middle East, and South Asia, which happen to be some of the fastest-growing gaming markets on the planet right now. If a game only feels good on a flagship, a big chunk of the audience never gets to find that out.

Start by Trimming the Build

Before anyone touches code, it’s worth just looking at what’s getting shipped. Switching from a universal APK to an Android App Bundle helps a lot, because Google Play can give each device only what it actually needs instead of forcing a budget phone to download textures meant for a Galaxy. Play Asset Delivery is a good companion to this too, so extra levels or language packs load later instead of bulking up the install.

Texture compression trips people up constantly. ASTC looks great and is efficient on newer GPUs, but older Adreno and Mali chips often can’t decode it, so an ETC2 fallback still has to be in there. Audio should be Vorbis, not raw WAV, because that adds up across dozens of sound effects fast. Running R8 or ProGuard strips dead code out automatically, but someone still has to go through the asset folder by hand. Every single project we’ve reviewed has had leftover textures, sounds, and prefabs from features that got cut months ago and just never got deleted.

Draw Calls, the Thing Nobody Mentions Until It’s Too Late

Here’s something that surprises a lot of developers. A game can be visually simple and still chug on a budget phone, because the slowdown isn’t coming from the art. It’s coming from draw calls, basically the small processing cost the renderer pays for every separate object it has to handle. On a flagship that cost barely registers. On an Adreno 506, it eats the frame budget before the “real” rendering even starts.

Fixing it is unglamorous but works. Combine sprites into atlases with something like TexturePacker. Turn on static batching for anything that doesn’t move. Use GPU instancing for stuff that repeats a lot, trees, coins, particles, enemies. Two scenes can look almost identical, one with 200 draw calls and one with 40, and the second runs noticeably better on weak hardware even though nothing visually changed.

Bake the Lighting and Move On

Real-time shadows and dynamic lighting look great in a trailer. They also tend to crush frame rates on cheap GPUs, no exceptions really. Baking lighting wherever it’s possible takes a lot of that pressure off. A separate shader variant for low-tier devices is worth building too, with vertex lighting instead of per-pixel, bloom and screen-space ambient occlusion cut, and post-processing trimmed close to nothing.

A working LOD system matters here, swapping distant objects for lower-poly versions automatically, plus occlusion culling so the engine stops rendering things the camera can’t even see. None of this is visible to players. They just notice the game doesn’t choke when a lot of stuff is happening on screen at once.

The Garbage Collection Problem That’s Hard to Pin Down

For teams working in Unity with C#, this one’s sneaky. Every time bullets, particles, or UI popups get created and destroyed on the fly, that builds up garbage the runtime has to clean up eventually, usually during combat, which is exactly when a hiccup is most noticeable. Object pooling solves most of it. Create those objects once, recycle them instead of constantly allocating new memory.

It also helps to skip LINQ and string concatenation inside Update loops, and to use structs instead of classes for small data that gets created a lot. None of this is exciting. It’s also, more often than people expect, the actual reason a game freezes for 200 milliseconds at the worst possible moment and nobody can figure out why.

Pick a Frame Rate and Don’t Chase 60 Everywhere

Not every device needs 60fps, and trying to force it across the board usually backfires. A tiered setup works better. 60fps on high-end hardware, a locked 30fps on low-end, dynamic resolution scaling adjusting render resolution on the fly to hold that target. Frame pacing matters a lot here too. A frame rate bouncing between 28 and 45 feels worse than a steady 30, even though the average might technically be higher.

Thermal throttling plays into this as well. A phone that overheats after ten minutes will throttle itself to protect the battery, and a game that runs fine early on and then tanks gets blamed for “bad optimization,” when really the hardware’s just doing its job.

Test on the Phones People Actually Have

This step gets skipped more than anything else on this list, and it’s the one that matters most. A flagship test device hides problems instead of revealing them. Android GPU Inspector and Unity’s Profiler or Frame Debugger help find the real bottlenecks, and validating fixes on actual budget hardware, Redmi, Realme, Samsung A-series, through Firebase Test Lab or a cloud device farm catches stuff that would otherwise only show up after launch. If QA is outsourced, giving them a specific list of low-tier devices to test on every single build makes a real difference compared to leaving it open-ended.

Performance Belongs in the Early Conversations Too

A lot of teams treat performance as a thing to deal with near the end. It works better earlier, especially during mobile game idea validation, right alongside questions about the core loop and monetization. Prototyping the gameplay loop on a mid- or low-tier device before locking in an art style or engine gives a much more honest picture of how the game will actually feel for most players. If an idea only really holds up at high settings on premium hardware, finding that out early is a lot cheaper than discovering it after months of art and code have already been built around it.

The Last Pass

Right before shipping, it’s worth running through everything once more. Android App Bundle, with ASTC and ETC2 both covered. Audio compressed, unused assets actually gone, not just ignored. Draw calls reduced through atlases and batching. Lighting baked, shaders simplified for low-tier devices. LOD and occlusion culling in place. Object pooling handling the stuff that gets created and destroyed constantly. Frame rate targets tiered, with dynamic resolution scaling backing them up. And all of it checked on real budget Android devices, not just whatever’s sitting on a desk in the office.

Going back to that tower-defense build, after a week of changes like these, the Redmi 9A went from 19fps to a steady 31. Not flashy. But it’s the difference between a player making it through the tutorial and one who doesn’t.

Leave a Reply

Your email address will not be published. Required fields are marked *