Mobile Architecture

Mobile Performance

Techniques for optimizing mobile app performance across startup time, frame rate, network efficiency, battery life, and memory management — with platform-specific tooling references.

⏱ 12 min read

What it is

Mobile performance optimization is the practice of ensuring a mobile application starts quickly, renders smoothly, uses the network efficiently, preserves battery life, and manages memory within the tight constraints of a mobile device. Unlike server applications where resources can be scaled horizontally, a mobile app must share CPU, memory, GPU, network radio, and battery with the OS and all other running applications on a fixed-hardware device. Performance degradation is immediately visible to users — a frozen frame, a janky scroll, or a slow startup is a first-class user experience defect.

Why it exists

Studies consistently show that mobile users abandon apps after a startup time exceeding 3–5 seconds, and that janky scrolling (dropped frames) is among the top reasons for negative reviews. Mobile hardware is also heterogeneous — a feature that runs smoothly on a flagship phone may be unusable on an entry-level device with a low-end CPU and limited RAM. Performance work must target the p90 device in the user base, not the developer's flagship test device.

When to use

Performance optimization applies continuously across the app lifecycle. Startup performance should be instrumented from day one; frame rate monitoring should run in CI (via UI test traces); network efficiency should be reviewed in API design. The techniques described here become particularly critical when an app has reached a meaningful user base, when performance regressions are appearing in crash/ANR reports, or when the app is targeting markets where entry-level Android devices are dominant.

Typical architecture


STARTUP PERFORMANCE
────────────────────
  Cold start: process creation → Application.onCreate() → Activity.onCreate()
              → inflate layouts → first frame drawn
  Warm start: process alive, Activity re-created (e.g., back from recents)
  Hot start:  process alive, Activity resumed from backstack (cheapest)

  Key optimizations:
  1. Move heavy work out of Application.onCreate() — use lazy initialization
  2. Use App Startup library (Android) / pre-main hooks (iOS) to sequence
     initialisation of 3rd party SDKs
  3. Defer non-critical SDK initialization until after first frame
  4. Android Baseline Profiles: pre-compile hot code paths with ART
     → Reduces cold start by up to 40% on first run after install
  5. iOS: reduce +load methods and static initializers in Objective-C
  6. Measure: Android reportFullyDrawn() API, iOS os_signpost

  Target: cold start < 2s (p90), warm start < 1s

FRAME RATE & RENDERING
───────────────────────
  Budget: 16.7 ms per frame for 60fps; 8.3 ms for 120fps (ProMotion/LTPO)
  Frame drop = main thread blocked > 1 frame budget

  Android tools:
  - Perfetto profiler: trace main thread, RenderThread, GPU
  - Systrace (legacy), Android Studio CPU profiler
  - Janky frames report in Play Console

  iOS tools:
  - Instruments → Core Animation / Time Profiler
  - MetricKit: onDiskWrite, cpuMetrics, animationMetrics in production

  Common causes of jank:
  - RecyclerView/UITableView: complex view hierarchy inflation on scroll
  - Bitmap decoding on main thread (use Glide/Coil/Kingfisher async)
  - Synchronous database reads on main thread
  - Layout inflation of deep view trees → prefer ConstraintLayout / SwiftUI

NETWORK EFFICIENCY
───────────────────
  1. HTTP/2: multiplexes requests; reduces connection overhead
  2. Response caching: Cache-Control headers; OkHttp disk cache; NSURLCache
  3. Compression: Accept-Encoding: gzip / br (Brotli); 50–80% size reduction
  4. Request coalescing: batch N requests into one; reduce radio activations
  5. Prefetching: predict next screen's data and fetch during idle periods
  6. Delta sync: fetch only changed records since last sync timestamp
  7. Image optimization: WebP format; serve appropriately sized images
     (avoid downloading 2048px image for a 64px avatar)

BATTERY OPTIMIZATION
─────────────────────
  Android Doze mode: restricts background activity when screen off + stationary
  → App receives maintenance windows (~15 min) for background work
  → Use WorkManager (respects Doze, battery saver) not raw AlarmManager

  iOS Background Modes:
  - Background fetch: system calls app periodically (adaptive frequency)
  - Silent push: triggers background fetch on demand (rate-limited)
  - Background URLSession: large downloads survive app suspension

  Avoid high-drain patterns:
  - Continuous location updates (use significant location change instead)
  - Keep-alive network polling (use push notifications + background fetch)
  - High-frequency sensor sampling without user interaction

MEMORY MANAGEMENT
──────────────────
  Android:
  - LeakCanary: detects Activity/Fragment/ViewModel leaks in debug builds
  - Memory Profiler (Android Studio): heap dumps, allocation tracking
  - onTrimMemory() callback: release caches when system requests it
  - Avoid static references to Contexts (common source of Activity leaks)

  iOS:
  - Instruments → Allocations / Leaks instruments
  - Xcode Memory Graph Debugger: visualise retain cycles
  - NSCache: automatic eviction under memory pressure
  - Weak references for delegate patterns to prevent retain cycles

ANR PREVENTION (Android)
─────────────────────────
  ANR (Application Not Responding): main thread blocked > 5s in background
  or > 200ms (broadcast) triggers system dialog
  → Fix: all I/O, network, and heavy computation off main thread
  → Use: Coroutines (Kotlin), RxJava, AsyncTask (deprecated → use coroutines)
  → Monitor: ANR rate in Play Console Vitals (target < 0.47%)

Pros and cons

Pros

  • Fast startup and smooth rendering directly correlate with user retention and store ratings.
  • Battery efficiency reduces user churn caused by users identifying the app as a battery drain and force-quitting it.
  • Network efficiency reduces backend costs and improves UX on slow connections.
  • Platform profiling tools (Perfetto, Instruments) provide detailed, actionable diagnostics without additional instrumentation in most cases.

Cons

  • Baseline profiles (Android) require regeneration whenever startup paths change, adding to CI maintenance.
  • Aggressive prefetching can increase server load and waste bandwidth if predictions are wrong.
  • Battery restrictions (Doze, background fetch) make some background use cases difficult to implement reliably.
  • Profiling on real low-end devices requires a device lab; emulators do not accurately represent the worst-case hardware performance.

Implementation notes

Baseline Profiles (Android): Android's ART runtime interprets bytecode until it JIT-compiles hot methods at runtime. Baseline Profiles capture which code paths are hot (via Macrobenchmark) and deliver pre-compiled profiles with the APK, so those paths are AOT-compiled on first install. Generate profiles using the BaselineProfileRule in the :baseline-profile Gradle module, targeting your critical user journeys (startup, main list scroll, checkout flow). Re-generate on every significant change to those journeys.

Image loading: Image decoding is one of the highest sources of memory pressure and frame drops on mobile. Always decode images asynchronously using a purpose-built library (Glide or Coil on Android; Kingfisher or SDWebImage on iOS). These libraries handle disk caching, downsampling to display size, memory cache with LRU eviction, and asynchronous decoding off the main thread. Never use BitmapFactory.decodeFile() or UIImage(named:) for large images on the main thread.

Common failure modes

  • Heavy Application.onCreate(): Initialising all SDKs synchronously in Application.onCreate() adds 300–800ms to cold start time; most SDKs can be lazily initialized on first use.
  • Main thread I/O: SQLite queries, file reads, or SharedPreferences commits on the main thread cause ANRs and jank; enable strict mode (StrictMode.enableDefaults() on Android) in debug builds to detect these.
  • Memory leaks via Context references: Holding a static reference to an Activity Context (often through a singleton that accepts a Context) prevents GC and causes memory to grow unboundedly across navigations.
  • Full list reloads: Replacing an entire RecyclerView adapter dataset instead of using DiffUtil causes full redraws; DiffUtil calculates the minimal diff and applies targeted updates, preserving animations.

Decision checklist

  • Is cold start time measured in CI using Macrobenchmark (Android) or XCTest metrics (iOS)?
  • Are Baseline Profiles generated for critical user journeys (Android)?
  • Is LeakCanary (Android) / Instruments Leaks (iOS) run as part of the development workflow?
  • Are all database, network, and file I/O operations performed off the main thread?
  • Is background work scheduled via WorkManager (Android) / BGTaskScheduler (iOS) to respect Doze and battery restrictions?
  • Is frame rate monitored with janky frames reports in Play Console / MetricKit?

Example use cases

  • Social media feed: Prefetch next page of posts when user is 10 items from the bottom; use DiffUtil for incremental list updates; decode images asynchronously at display size; defer analytics SDK initialization to after first frame.
  • Ride-hailing app: Baseline Profiles for map initialization (heavy startup path); use significant location changes instead of continuous GPS when trip is not active; load map tiles asynchronously off main thread.
  • Mobile API Design — Network efficiency techniques (payload optimization, HTTP/2, request coalescing) are designed at the API layer.
  • Offline-First Design — Local caching and offline-first patterns eliminate network round trips and improve perceived performance.
  • Mobile Architecture Patterns — ViewModel lifecycle management (MVVM) prevents leaks and redundant work across configuration changes.

Further reading