Optimize mobile app performance for smooth 60fps experiences. Covers startup time, memory management, battery optimization, network efficiency, and profiling tools for iOS and Android.
Mobile Performance Optimization is the disciplined practice of reducing latency, minimizing memory pressure, and ensuring responsive interaction across device types and network conditions. It matters when app load times exceed 3 seconds, when animations stutter on 100ms frames, or when users abandon the app after a single slow screen transition. This page details the actions, tools, and tradeoffs that define a high-performing mobile experience, grounded in real configurations, common failure modes, and precise workflows.
Before any change, measure the actual performance of the app in real environments. Use tools that capture real-world metrics across CPU, memory, network, and rendering.
On Android, use systrace with the --section=app and --async flags to record a 10-second trace during a typical user session.
adb shell cmd perfetto start --section=app --async --duration=10000 --output=/sdcard/perfetto-trace.trace
On iOS, use Instruments with the Time Profiler and Core Animation templates, exported as .trace files. In CI, automate this with xcrun instruments:
xcrun instruments -t "Time Profiler" -D build/profile.trace -k "CPU Time" -D build/profile.trace -w "iPhone 14" -e "App Launch" -e "User Interaction" MyAppUITests
The trace file contains a timeline with:
Use Perfetto (open-source, built on Ftrace and LTTng) to analyze the trace. In the Perfetto UI, filter by com.example.myapp and examine the RenderFrame events. A frame taking 25ms indicates jank, and RenderFrame events with gaps exceeding 16ms show dropped frames.
In code, ensure ViewRootImpl or Scene updates are not blocked by onDraw() or layout().
On Android, use Memory Profiler in Android Studio or adb shell dumpsys meminfo <package>. The output includes:
adb shell dumpsys meminfo com.example.myapp
Output snippet:
App Memory Usage (in KB):
TOTAL RSS PSS SWAP
Native 120,000 105,000 98,000 15,000
Dalvik 80,000 72,000 68,000 12,000
Graphics 25,000 22,000 20,000 8,000
Other 18,000 15,000 14,000 6,000
Total 243,000 214,000 200,000 41,000
Use GC events from adb logcat to detect long pauses:
adb logcat -s GC
A GC_CONCURRENT taking 200ms with a GC_FULL every 30 seconds indicates memory pressure.
On iOS, use Allocations and Time Profiler in Instruments to detect:
UIViewController lifecyclesUITableView cellsCombine or RxSwift pipelinesThe app launch time is the first impression. Optimize it by reducing the time from app launch to first visible frame.
On Android, ensure android:launchMode="singleTop" or singleTask and define android:windowBackground in styles.xml.
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowContentTransitions">true</item>
</style>
</resources>
Use android:windowBackground with a LayerListDrawable that includes a static background and a ProgressBar for smooth transitions.
In AndroidManifest.xml, set:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
On iOS, use LaunchScreen.storyboard and LaunchImage assets. In Info.plist, define UILaunchStoryboardName and UIMainStoryboardFile. Ensure the main function in AppDelegate.swift is not blocked by asynchronous setup.
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Critical: Do not do network calls or heavy work here
setupDependencies()
setupUI()
return true
}
}
Use ViewHierarchy and Core Animation in Instruments to ensure the first render is not blocked by layout() or draw().
On Android, defer non-critical work with Handler, WorkManager, or CoroutineScope:
lifecycleScope.launch(Dispatchers.Main) {
// Render first screen
renderHomeScreen()
// Background work
async(Dispatchers.IO) {
fetchUserData()
loadRecentPosts()
prefetchImages()
}
}
On iOS, use DispatchQueue with background priority and async calls:
DispatchQueue.global(qos: .background).async {
self.fetchUserData()
self.loadRecentPosts()
self.prefetchImages()
}
In WebView or WKWebView, use preconnect and prefetch for critical resources:
<link rel="preconnect" href="https://api.example.com" />
<link rel="prefetch" href="/feed.json" />
<link rel="prefetch" href="/styles.css" />
Animations should be 60fps with minimal jank. Achieve this by reducing frame cost and ensuring the UI thread is not blocked.
On Android, use ViewPropertyAnimator or ObjectAnimator with setDuration(16) for 60fps animations. Use TransitionManager to update multiple views in a single frame.
ViewGroup container = findViewById(R.id.container);
Transition transition = new ChangeBounds();
transition.setDuration(16);
TransitionManager.beginDelayedTransition(container, transition);
// Update child views
In RecyclerView, use setItemAnimator() with DefaultItemAnimator and setSupportsChangeAnimations(true).
On iOS, use UIView.animate(withDuration:animations:) with a duration of 0.166 seconds (16.6ms).
UIView.animate(withDuration: 0.166, animations: {
self.imageView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
})
Ensure CALayer properties are animated using implicit animations or keyframe animations, not setNeedsLayout() or setNeedsDisplay().
On Android, use Choreographer to detect frame drops:
Choreographer.getInstance().registerFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
long frameTimeMs = frameTimeNanos / 1_000_000;
if (frameTimeMs > 17) { // 17ms = 58.8fps
Log.d("Performance", "Frame dropped: " + frameTimeMs + "ms");
}
// Schedule next callback
Choreographer.getInstance().postFrameCallback(this);
}
});
On iOS, use CADisplayLink to track frame times:
let displayLink = CADisplayLink(target: self, selector: #selector(updateFrame))
displayLink.add(to: .main, forMode: .default)
Use Core Animation to profile draw time and compositing time. A compositing time exceeding 3ms indicates heavy layer merging.
Reduce data transfer and improve load times for remote resources.
Use HTTP/2 with push and priority. On Android, use OkHttp with ConnectionPool and Cache.
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
.cache(new Cache(new File(context.getCacheDir(), "http-cache"), 10 * 1024 * 1024)) // 10MB
.build();
In HTTP/2, enable server push to send related resources in a single stream.
On iOS, use URLSession with background configuration and taskPriority:
let config = URLSessionConfiguration.background(withIdentifier: "com.example.myapp.sync")
config.sessionSendsLaunchEvents = true
let session = URLSession(configuration: config)
Use HTTP/3 (QUIC) where available. On Android, enable HTTP/3 with OkHttp:
OkHttpClient client = new OkHttpClient.Builder()
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_3))
.build();
Use WebP for images, AVIF for high-quality assets, and SVG for vector graphics.
On Android, use BitmapFactory.Options with inPreferredConfig and inPurgeable:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGBA_8888;
options.inPurgeable = true;
options.inInputShareable = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.profile_image, options);
On iOS, use ImageIO and CoreGraphics to compress and downscale images:
let imageData = UIImageJPEGRepresentation(image, 0.8)
let compressedImage = UIImage(data: imageData!)
Use Asset Catalog with Scale Factor and Size Class to deliver appropriate assets. In Info.plist, define CFBundleIcons with CFBundlePrimaryIcon and CFBundleIconFiles.
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>Icon-60@2x</string>
<string>Icon-60@3x</string>
<string>Icon-76</string>
<string>Icon-76@2x</string>
<string>Icon-76@3x</string>
</array>
</dict>
</dict>
RecyclerView: ViewHolder instances not cleared, onBindViewHolder not using ViewHolder correctly. Use LeakCanary to detect and report leaks.onDraw(): View onDraw() methods doing heavy work. Use android:hardwareAccelerated="true" and setLayerType(LAYER_TYPE_HARDWARE, null) on View to offload drawing to GPU.OkHttp Cache not properly configured. Use Cache-Control headers with max-age and must-revalidate.Core Animation: UIView frame, center, or transform updates not being animated via CALayer. Use UIView.animate(withDuration:animations:) with UIViewPropertyAnimator for complex sequences.windowBackground: MainActivity shows blank screen before onCreate() completes. Ensure android:windowBackground is set and MainActivity uses setContentView() in onCreate().Performance optimization is not a one-time task. It is a continuous process of measuring, diagnosing, tuning, and validating. The best-performing apps are those that anticipate the user’s next action and deliver it before they ask.
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