Optimize mobile app performance for smooth 60fps rendering, fast startup, efficient memory usage, and minimal battery drain. Covers profiling tools, rendering optimization, memory management, network efficiency, and the metrics that matter.
This reference page covers the engineering practices, tools, and configuration patterns used to identify, measure, and reduce performance costs in mobile applications. It matters whenever an app feels unresponsive, consumes excessive battery, or exceeds resource budgets on target devices. Performance problems in mobile are especially insidious because they compound: a single slow network request inflates CPU time, keeps the radio active longer, and delays UI rendering, creating a cascading effect that degrades perceived responsiveness far beyond the original latency.
Frame timing is the most immediate metric users notice. On Android, adb shell dumpsys gfxinfo <package> outputs a frame timeline including vsync, frame, input, and draw timestamps. A frame interval consistently above 16.67ms indicates the app is missing the display’s vertical refresh cycle, resulting in visible stutter. For per-app frame profiles during a recorded interaction session, adb shell am profile start -w <package> com.example.app begins CPU profiling and writes a .trace file analyzable with systrace or chrome://tracing. On iOS, idevicesyslog -s combined with Xcode’s Instruments “Time Profiler” target provides similar wall-clock frame timing, but the defaults command can toggle IDESurfaceFPS to overlay FPS counters directly on the simulator’s status bar for ad hoc checks.
Sharp edge: dumpsys gfxinfo only reports the most recent 100 frames. If a session includes long garbage-collection pauses or ANR-inducing main-thread blocks earlier in the trace, those frames may be dropped from the output, giving a falsely optimistic view of frame pacing. Always pair this with dumpsys meminfo to confirm whether memory pressure is the root cause.
Startup is measured from process launch to first UI interaction. On Android, adb shell setprop debug.startup.activity <package/.MainActivity> combined with dumpsys activity timestamps the transition through each activity state. The am start command with -W flags wait-for-draw and reports TotalTime in the output. For React Native or Expo projects, npx react-native start --reset-cache ensures a clean Metro bundle, and adb shell dumpsys package <package> | grep ACTIVITY confirms which activity is being launched. On iOS, defaults write com.apple.CoreSimulator.CoreSimulatorService AllowFirstLaunch can disable first-run animations for consistent measurement, but this flag resets on simulator restart.
Confused option: --reset-cache is often conflated with --clear-stored-cache. The former rebuilds the Metro bundler cache from source; the latter deletes stored module files. Running only --clear-stored-cache may leave outdated bytecode in the bundler’s memory, producing no observable startup improvement despite the flag appearing to succeed.
Network performance directly affects Time-to-Interactive and battery life. curl -w "\n\nTime connect: %{time_connect}\nTime start-transfer: %{time_starttransfer}\nTime total: %{time_total}" -o /dev/null https://example.com/api/data prints per-phase timings useful for baseline measurement. In production code, fetch with keepalive: true reuses a TCP connection across requests, but on Android WebView, setNetworkAvailable(true) must be called before the first request to avoid spurious ERR_INTERNET_DISCONNECTED errors on emulated networks.
Silently failing pattern: Setting Cache-Control: max-age=86400 on responses does not guarantee the client respects it if ETag headers are absent. Some CDNs omit ETag for dynamically generated payloads, causing the client to re-download every request regardless of the cache directive. The fix is to ensure origin responses include both ETag and Last-Modified, or to use Vary: Accept-Encoding when serving compressed variants.
Memory leaks in mobile apps often manifest as gradual ANR rates or OOM kills on low-end devices. Android’s adb shell dumpsys meminfo <package> outputs Native Heap, Dalvik Heap, and Other columns. A Native Heap growth of >5MB per hour without corresponding Java GC cycles typically indicates a C++ or OpenGL resource leak. For Java-side leaks, adb shell hprof-conv <file.hprof> && jmap -histo:live <pid> generates a live histogram, but the hprof-conv binary is only available in Android SDK Platform-Tools r30+; older SDK versions require jstack <pid> > stacktrace.txt for thread dumps, which does not capture heap allocation counts.
Production breakage: Enabling android:largeHeap=true in the manifest to accommodate temporary memory spikes can mask leaks and cause the system to kill the process at lower memory thresholds because the kernel no longer considers the app a candidate for memory reclamation. Always profile without the flag first, and only enable it as a last-resort mitigation for legacy code paths.
Wakelocks keep the CPU active while the screen is off, draining battery. dumpsys power lists all active wakelocks with timestamps; dumpsys battery reports health, temperature, and present status. The dumpsys cpuinfo output shows CPU usage per process, but on Android 12+, dumpsys cpuinfo requires RUN_AS privileges—root or adb shell su -c dumpsys cpuinfo is needed. On iOS, sysdiagnose -u captures a system trace including CPU and Energy metrics, but the output is a binary plist requiring python3 -c "import plistlib; print(plistlib.loads(open('/var/log/sysdiagnose/energy.log').read()))" to parse.
Confused flag: android:debuggable=true in the debug build variant does not directly increase CPU overhead; it enables JIT compilation and extra safety checks. However, leaving it true in production APKs (via mistaken merge of source sets) inflates binary size and can trigger Play Store rejection for non-compliant debug flags in release artifacts.
For frameworks that compile to native binaries (e.g., Kotlin Multiplatform, React Native, Flutter), bundle size and startup jitter are platform-specific concerns. Metro’s maxWorkers config key controls the number of parallel workers for transformer hashing; setting it to os.cpus().length maximizes CPU utilization on build servers but can cause local developer machine contention, slowing npm start feedback loops. The metro-config JSON format accepts resolver.blockList to exclude directories from hashing—useful for removing node_modules folders that never change between runs.
Sharp edge: enableHermes in React Native 0.71+ swaps the JavaScript engine from V8 to Hermes, reducing bundle size and improving cold-start time on Android. However, Hermes’ garbage collector uses a different heap layout, and existing LeakCanary configurations may report false positives because the collector defers collection cycles differently than V8. The migration path requires updating LeakCanary’s refWatcher implementation to Hermes’ HeapDump API, which is not documented in the standard React Native upgrade guide.
These no-code/low-code platforms expose performance knobs through their respective UIs—OutSystems’ “Service Studio” memory thresholds, Mendix’s “SLA timers,” and Retool’s “Query timeout” settings—but the underlying mechanics (connection pooling, bytecode minification, runtime scheduling) follow the same patterns described above. Mapping those UI controls to the concrete commands and config keys listed here gives you a concrete migration path should you need to drop down to native code for a specific performancecritical path.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy