Mobile Performance Optimization

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.

Core Workflow: Measure Before You Optimize

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.

Capture Performance Traces

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:

Analyze Frame-by-Frame Latency

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().

Profile Memory Usage

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:

Optimization: Reduce Load Time

The app launch time is the first impression. Optimize it by reducing the time from app launch to first visible frame.

Optimize App Launch

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
    }
}

Optimize First Render

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" />

Optimization: Smooth Animations and Responsiveness

Animations should be 60fps with minimal jank. Achieve this by reducing frame cost and ensuring the UI thread is not blocked.

Optimize Animation Performance

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().

Reduce Jank and Frame Drops

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.

Optimization: Network and Asset Efficiency

Reduce data transfer and improve load times for remote resources.

Optimize Network Requests

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();

Optimize Image and Asset Delivery

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>

Common Failure Modes and Sharp Edges

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.