Unity Mobile Performance Optimization: A 2026 Guide
PGP Studio Team · April 10, 2026 · 10 min read
Practical performance fixes that took our last release from a stuttering build to a locked 60 FPS.

Frame drops destroy casual gaming experiences instantly. If an app stutters when a block aligns, players uninstall within minutes, and garbage collection spikes are the most common, and most fixable, cause. This is the exact sequence of fixes we ran against our own catalog to take a stuttering build to a locked 60 FPS on mid-range Android hardware, in the order we found them, from most to least impactful.
Garbage Collection Is the First Thing to Fix, Not the Last
Treat every per-frame allocation as a bug. Favor structs over classes for short-lived data, and pool anything that spawns and despawns repeatedly (particles, popups, projectiles) instead of instantiating and destroying it. Object pooling is consistently the single most effective technique for eliminating GC-driven stutter on mobile. As a target, keep garbage collection allocations under roughly 1KB per frame; go much higher and it shows up as a visible hitch the moment the collector runs.
The reason this comes first in every optimization pass we run is that GC stutter is uniquely deceptive to profile casually. A build can run at a smooth 60 FPS for seconds at a time and then drop several frames the instant the collector sweeps, and if you're only watching an average FPS counter rather than a frame-time graph, that spike gets lost in the average. Always profile with a frame-time graph, not a single FPS number, specifically because it's the spikes that players feel, not the average.
The Unity Profiler's GC Alloc column, viewed per-frame rather than averaged, is the fastest way to find the worst offenders. Sort by allocation size across a representative play session and you'll typically find that a small number of call sites, often something as unglamorous as a LINQ query running every frame inside an Update() loop, or a string concatenation building a UI label every tick, account for the overwhelming majority of garbage generated. Fix those first; they're usually a few hours of work each and the FPS improvement is immediate and visible in the profiler the moment you ship the fix.
Consolidate Your Update Loops
Avoid calling Update() independently on hundreds of active MonoBehaviours. Route per-frame logic through a single unified controller loop instead: fewer call-site transitions means less overhead per frame. Unity's own engine-level overhead per MonoBehaviour.Update() call is small individually, but it's not zero, and on a board with 200 active tile objects each running their own Update(), that overhead adds up to a measurable chunk of your frame budget before you've done any actual game logic.
The pattern we use across our own puzzle titles is a single manager class that iterates over a plain list or array of active game objects and calls an interface method on each, rather than relying on Unity's built-in message dispatch. This also has a secondary benefit beyond raw speed: it makes profiling easier, because all your per-tile logic shows up under one call stack in the profiler instead of being scattered across hundreds of separate Update() entries that are individually too small to spot but collectively significant.

Batch Aggressively
Static and dynamic batching plus mesh combining cut draw calls under complex scenes, and ASTC texture compression keeps GPU memory bandwidth in check on modern Android and iOS hardware. Draw call count matters more on mobile GPUs than desktop developers coming from a PC background tend to expect, because mobile tile-based GPU architectures pay a real per-draw-call cost that desktop immediate-mode renderers mostly hide.
Static batching is close to free: mark any non-moving geometry (background tiles, static UI frames, level decoration) as static in the inspector and let Unity combine it automatically at build time. Dynamic batching requires more discipline, since it only kicks in for objects sharing a material and under specific vertex-count thresholds, so standardizing on a small number of shared materials across similar object types pays off directly in batching eligibility, not just art consistency.
On the texture side, ASTC is the right default compression format for both Android and iOS in 2026; it gives you a single format to author against instead of maintaining separate ETC2 and PVRTC pipelines for each platform, and the compression ratio at equivalent visual quality is meaningfully better than either of the older platform-specific formats it replaces.
Shader Complexity Is an Easy Blind Spot
It's easy to profile CPU-side allocations and draw calls carefully while never actually checking shader cost, because a shader that renders fine on a development device (usually a mid-to-high-end phone used by whoever's building the game) can be meaningfully more expensive on the low-end Android hardware that makes up a large share of a casual game's actual install base. Use Unity's Frame Debugger to check per-shader render cost specifically on a low-end test device, not just your dev phone.
For 2D casual and puzzle games specifically, the most common shader-cost mistake we see is using an unnecessarily complex shader (often inherited from a default asset store package) for simple sprite rendering that could run on Unity's lightweight built-in 2D sprite shader instead. Swapping an overbuilt shader for a simpler one that produces visually identical output on a 2D sprite is one of the highest ratio of effort-to-improvement fixes available, because the change is often a single material reassignment with zero gameplay risk.
Physical Device Testing Is Not Optional
A game that runs at 60 FPS on a flagship device and stutters on a mid-range one isn't finished. We profile on physical hardware, not just the Editor or a high-end test device, because the Unity Editor's performance characteristics diverge from an actual mobile build in ways that matter: garbage collection behaves differently, and the Editor simply has more headroom than a three-year-old mid-range Android phone running your game alongside a dozen background OS processes.
Maintain a small physical device lab spanning at least one genuinely low-end Android device, one mid-range Android device, and one representative iOS device, and profile builds on all three before every release, not just the flagship device the team happens to carry. The mid-range Android tier specifically is where most casual game performance problems actually surface for players, since it represents the largest share of the install base for most genres outside premium markets.
Loading and Memory Footprint Matter as Much as Frame Rate
Frame rate gets the most attention because it's the most visible symptom, but memory footprint and load time are just as capable of losing a player, especially on budget devices with limited RAM. An app that gets silently killed by the OS for memory pressure while backgrounded, then has to fully reload when the player switches back, reads to that player as a crash even though technically it isn't one.
Address this with texture atlasing to reduce draw calls and memory overhead together, aggressive audio compression for anything that isn't a critical sound cue, and explicit unloading of scene assets that aren't needed in the current game state rather than relying entirely on Unity's automatic memory management to catch everything. Profiling total memory footprint on your lowest-RAM target device, not just frame rate, should be a standard part of every release checklist.
Build Settings Are Free Performance Most Teams Leave on the Table
IL2CPP with the highest applicable code stripping level should be the default for release builds, not an occasional experiment. The switch from Mono to IL2CPP is close to a solved problem in 2026 and generally produces faster execution for CPU-bound game logic, at the cost of longer build times that only affect the development team, not the player. If a project is still shipping on Mono for release builds out of inertia, that's a free performance gain sitting unclaimed.
Beyond the scripting backend, the managed code stripping level and the specific .NET API compatibility level chosen both affect final build size and, indirectly, load time on constrained devices. Aggressive stripping occasionally breaks reflection-dependent third-party plugins, which is the usual reason teams back off to a safer setting and never revisit it, but it's worth the one-time investment to identify exactly which plugin needs an exception rather than disabling aggressive stripping project-wide.
Physics and Collision Checks Add Up Fast in Puzzle Games
It's a common assumption that 2D puzzle and casual games are too simple to have physics-related performance problems, but collision checks between many simultaneously active objects (falling blocks, matched tiles, cascading chain reactions) can quietly become one of the more expensive parts of a frame if left on default physics settings. Reducing the physics timestep frequency for game types that don't need frame-perfect physics response, and using simplified collider shapes rather than precise mesh colliders for gameplay elements that only need rough overlap detection, both reduce this cost without any visible change to how the game plays.
For grid-based games specifically, it's often faster to skip Unity's physics engine entirely for collision and overlap logic and implement simple array-index-based adjacency checks instead, since a grid position lookup is computationally trivial compared to a general-purpose physics collision check, and grid games rarely need the physics engine's generality. This is one of the more project-specific optimizations on this list, but it's consistently one of the highest-impact ones for exactly the block-matching and line-clearing genre we build most often.
What a Full Pass Actually Looks Like
On a real project, this isn't five isolated fixes applied once. It's a repeatable sequence: profile on physical low-end hardware first to find where the frame-time graph actually spikes, fix the worst GC offenders by call-site allocation size, consolidate Update() loops where object counts are high, check shader cost specifically on the same low-end device, and re-profile before calling it done. Running that sequence at the end of every major feature addition, not just once before launch, is what keeps a game from slowly regressing back into stutter as content gets added over its live lifetime.
None of these techniques are exotic or Unity-version-specific; they're fundamentals that matter more, not less, as mobile hardware diversity keeps widening at the low end. The studios that consistently ship smooth casual games aren't using secret tools the rest of the industry doesn't have access to, they're just running this checklist disciplined and often, on real devices, every single release.
The discipline part is the part that actually differentiates studios in practice, not the technique list itself. Every fix described above is documented in Unity's own performance guidance and widely known across the industry; the gap between a game that stutters and one that doesn't is almost never a missing technique, it's a missing habit of re-profiling on real low-end hardware after every content addition instead of assuming last release's optimization work still holds once new art, new levels, and new systems have been layered on top of it. We consider this the single most transferable lesson across every optimization pass we have run: profile on the actual device tier your real players use, not your development hardware, before trusting any assumption about where the bottleneck actually lives.
Shipping the same build to the web adds its own separate profiling questions, and our Unity WebGL Build Optimization guide picks up right where mobile leaves off. This is the same performance bar we hold every build to on our own Unity development engagements, not just our published catalog.
Need Unity development help?
From prototype to store-ready release, this is exactly what we do for client engagements.
Explore Unity Development