Mobile Performance Optimization Guide
Users are impatient. Studies show 53% of users abandon apps that take longer than 3 seconds to load. And it's not just loading. Janky scrolling, laggy animations, and battery drain will earn you one-star reviews and quick uninstalls.
Performance isn't a nice-to-have. It's table stakes. Let's fix it.
Measure Before You Optimize
First rule: don't guess. Measure. You'll be surprised where actual bottlenecks are versus where you assumed they'd be.
Key Metrics
Launch time: Time from tap to interactive. Under 2 seconds is good. Under 1 second is great.
Frame rate: Target 60 FPS for smooth scrolling and animations. Dropped frames cause visible jank.
Memory usage: High memory triggers system kills. iOS terminates background apps aggressively. Android has OOM killer.
Battery impact: Users notice which apps drain their battery. They uninstall those apps.
Network efficiency: Data usage, request count, payload sizes. Matters especially for users on limited plans.
Profiling Tools
iOS: Xcode Instruments. Time Profiler for CPU, Allocations for memory, Core Animation for rendering, Energy Log for battery. These are incredibly powerful and free.
Android: Android Studio Profiler. CPU, Memory, Network, Energy tabs. Also check Android Vitals in Play Console for field data.
Cross-platform: React Native has Flipper. Flutter has DevTools. Both show what's happening in native and framework layers.
Run profiling on real devices, not simulators. Simulators have different performance characteristics.
Launch Time Optimization
First impressions matter. A slow launch feels like a broken app.
What Happens at Launch
Process creation, dylib loading, runtime initialization, main() execution, UI construction, first frame render. Each phase can be optimized.
Reduce Pre-main Time
- Fewer dynamic libraries: Each dylib adds loading time. Consolidate where possible.
- Avoid static initializers: Code that runs before main() blocks launch.
- Strip unused code: Dead code still gets loaded. Remove it.
Speed Up Initial Render
- Defer non-essential work: Don't load everything on launch. Load what's needed for the first screen, defer the rest.
- Use launch screens effectively: A static launch screen buys you time while the real UI loads.
- Avoid blocking network calls: Never wait for a network request before showing UI. Show cached data or skeleton screens.
Profile Your Launch
On iOS, set DYLD_PRINT_STATISTICS in scheme to see pre-main breakdown. On Android, use adb shell am start with -W flag to get launch time.
Rendering Performance
Smooth 60 FPS means each frame has 16.67 milliseconds to render. Miss that deadline and users see stuttering.
Common Culprits
Overdraw: Drawing the same pixels multiple times. Use GPU profiler to visualize. Simplify view hierarchies, avoid unnecessary backgrounds.
Complex layouts: Deep view hierarchies and expensive constraint calculations. Flatten where possible. Use RecyclerView (Android) or UICollectionView (iOS) for lists.
Main thread work: Anything on the main thread competes with rendering. Move computation, parsing, and I/O to background threads.
Large images: Don't render a 4000px image in a 400px container. Resize before display. Load appropriate resolution for device.
Scrolling Performance
Lists are where rendering issues hurt most. For smooth lists:
- Reuse cells (recycling). Never create new views while scrolling.
- Fixed cell heights are faster than dynamic.
- Load images asynchronously. Never block scrolling for network.
- Simplify cell layouts. Complex shadows and round corners cost frames.
- Prefetch data before it's needed.
Memory Management
Mobile devices have limited RAM. Use too much and the system kills your app.
Finding Leaks
Memory leaks accumulate over time. Use Instruments Leaks tool (iOS) or LeakCanary (Android) to find them.
Common leak patterns:
- Retain cycles from closures/lambdas capturing self
- Forgetting to unregister observers/listeners
- Singletons holding references to views or controllers
Image Memory
Images are the #1 memory consumer in most apps. A 12MP photo is ~48MB uncompressed in memory.
- Don't keep full-resolution images in memory if displaying thumbnails
- Aggressively cache to disk, load on demand
- Use image loading libraries (SDWebImage, Glide, Kingfisher) that handle this automatically
- Clear caches when receiving memory warnings
Memory Warnings
Both iOS and Android send warnings before killing apps. Listen to them. Clear caches, release optional resources, persist state. This can save your app from termination.
Network Optimization
Network requests are slow, unreliable, and drain battery. Minimize them.
Reduce Requests
- Batch multiple requests into single calls where API allows
- Use pagination. Don't fetch 1000 items when 20 are visible.
- Cache aggressively. HTTP caching headers, local database, whatever makes sense.
Reduce Payload Size
- GZIP compression (most servers support this automatically)
- Request only fields you need (GraphQL is great for this)
- Appropriate image resolutions. Don't send 4K images to phones.
- Use modern formats: WebP for images, protocol buffers for data
Handle Offline Gracefully
Cache what matters. Show stale data while fetching fresh. Never leave users staring at a spinner because the network is slow.
Battery Optimization
Battery drain gets your app named in "Battery Usage" settings. That's not where you want to be.
Big Drains
- GPS: Continuous location tracking kills batteries. Use significant change monitoring when possible. Lower accuracy when exact location isn't critical.
- Network: Radio activation is expensive. Batch requests. Use background fetch APIs instead of polling.
- CPU: Sustained processing drains battery and heats the device. Do work in bursts, then let CPU idle.
- Screen: This is actually the biggest drain, but it's user-controlled. You can help by supporting dark mode (OLED screens use less power on black).
Background Execution
Both platforms limit background execution to save battery. Work within the system:
- Use background fetch for periodic updates
- Use push notifications to wake the app only when needed
- Complete tasks quickly when given background time
- Don't abuse background modes (apps get rejected for this)
Platform-Specific Tips
iOS
- Enable Whole Module Optimization for release builds
- Use
lazyproperties to defer initialization - Profile with Energy Impact in Instruments
Android
- Enable R8/ProGuard for code shrinking
- Use Android App Bundle for smaller downloads
- Check Android Vitals for field performance data
- Avoid
StrictModeviolations
Continuous Monitoring
Performance regresses. What's fast today might be slow after a few releases of feature additions.
Set up automated performance testing. Track launch time, frame rates, and memory usage per release. Firebase Performance Monitoring, New Relic, and Embrace can track field performance.
Set budgets and alerts. "If launch time exceeds 2 seconds, fail the build." This catches regressions before users do.
The Bottom Line
Performance is a feature. Maybe the most important feature. Users don't articulate "this app is slow" in feedback. They just leave.
Measure first. Fix the real bottlenecks. Optimize images and network. Respect memory limits. Monitor continuously.
A fast app feels professional. It feels like you care. That's the bar. Meet it.