Overview
The RAIL performance model (Response, Animation, Idle, Load) adapted for mobile: Response to user input under 100ms (no perceptible lag). Animation at 60fps minimum (16.67ms frame budget). Idle time used for background work that does not consume the main thread. Load (cold start) under 2 seconds on the median device. These targets are specified as requirements, measured continuously in CI, and monitored in production.
Cold start begins when the OS creates the application process and ends when the first frame is visible. The two phases: pre-main (Android: class loading, DexOpt, static initializers; iOS: dylib linking, ObjC runtime setup, static initializers) and post-main (Application.onCreate / AppDelegate.application:didFinishLaunchingWithOptions, DI graph construction, first Activity/UIViewController creation).
Android optimizations: Baseline Profiles generate Ahead-of-Time compilation hints for the Play Store installer, reducing cold start by 30% by pre-compiling hot methods and classes. Generated with Jetpack Macrobenchmark in CompilationMode.Full. App Startup library parallelises Initializer execution. Hilt: avoid heavy work in singleton constructors.
iOS optimizations: Reduce dylib count (use static libraries where possible). Eliminate or defer static initializers. Use lazy initialization for expensive objects. Measure with Instruments Time Profiler — the pre-main and post-main timeline is visualised directly.
Frame Rate and Jank
The 16.67ms frame budget covers: measure (layout calculation), draw (composable execution / view drawing), submit (GPU command submission). Any work on the main thread that exceeds the budget causes a dropped frame — visible to the user as stutter.
Compose recomposition optimization: remember {} caches expensive calculations, avoiding recomputation on every composition. derivedStateOf {} creates computed state that only re-emits when the computed value changes — preventing downstream recompositions from upstream state changes that don't affect the downstream computation. key() in LazyColumn items ensures correct reuse of composables when list items move.
iOS Core Animation: offscreen rendering (red overlay in Instruments) indicates a CALayer property requires an offscreen compositing pass. Common causes: cornerRadius with masksToBounds, non-opaque views, shouldRasterize. Blended layers (green overlay): semi-transparent views requiring alpha compositing.
Memory Management
Android GC pauses cause jank — avoid object allocation in draw code (Canvas.drawBitmap(), custom View onDraw()). Use object pooling for frequently allocated objects in RecyclerView. Detect memory leaks with Android Studio Memory Profiler heap dumps and LeakCanary library. LeakCanary hooks into the GC and surfaces retained objects with stack traces.
iOS ARC retain cycles: a closure that captures self strongly, stored in a property of self, creates a retain cycle — both objects hold strong references and neither is deallocated. Fix with [weak self] or [unowned self] in the closure capture list. Use Xcode Memory Graph Debugger to visualise the object graph and identify cycle participants.
Battery Optimization
Every wakeup has a cost. Batch background work using WorkManager (Android) or BGTaskScheduler (iOS) rather than scheduling frequent small tasks. Use significant-change location API instead of continuous GPS tracking when exact location is not required continuously. Avoid wake locks outside of active processing — use WorkManager's constraint system to run background work only when the device is charging or on Wi-Fi.
Anti-Patterns to Avoid
⚠ 1. I/O on Main Thread
Reading from SharedPreferences, database queries, or network calls on the main thread. Produces ANR on Android (5 seconds), hangs the UI on iOS. Detected by StrictMode (Android) and Thread Sanitizer (iOS) in debug builds.
Hover to see the fix ↻
↺ Correct Approach
Dispatchers.IO for all disk and network operations. Dispatchers.Default for CPU-intensive work. Main thread reserved exclusively for UI rendering and user interaction handling.
⚠ 2. Unnecessary Recomposition
A Compose function that reads from a large StateFlow object recomposes every time any field in that object changes, even fields it does not use.
Hover to see the fix ↻
↺ Correct Approach
Decompose large state into smaller, independent state objects. Use derivedStateOf to compute derived values that only change when relevant inputs change.
Flowchart
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%%
flowchart TD
subgraph STARTUP["🚀 Startup Performance — Target < 2.0s P50"]
direction LR
BP["<b>Baseline Profiles</b><br/>30% cold start improvement<br/>Pre-compiled hot paths<br/>Play Store AOT installation"]
ASL["<b>App Startup Library</b><br/>Parallel Initializer execution<br/>Deferred non-critical init<br/>Lazy DI singletons"]
INS["<b>Instruments (iOS)</b><br/>Pre-main timeline<br/>Static initializer audit<br/>dylib count reduction"]
end
subgraph FRAME["🎬 Frame Performance — 60fps · 16.67ms Budget"]
direction LR
RC["<b>Recomposition Control</b><br/>remember() · derivedStateOf<br/>key() in LazyColumn<br/>stable annotations"]
SM["<b>StrictMode</b><br/>Main thread I/O → crash<br/>Debug builds only<br/>Zero tolerance policy"]
GPU["<b>GPU Profiling</b><br/>Offscreen rendering audit<br/>Blended layers check<br/>Core Animation instrument"]
end
subgraph MEMORY["💾 Memory — ≤ 150MB RSS"]
direction LR
LC["<b>LeakCanary</b><br/>Retained object detection<br/>Retain cycle visualization<br/>Every debug build run"]
HD["<b>Heap Dumps</b><br/>Android Studio Profiler<br/>Xcode Memory Graph<br/>Object allocation tracking"]
end
subgraph MEASURE["📏 Measurement — CI Enforced"]
direction LR
FBP2["<b>Firebase Performance</b><br/>P50 · P75 · P90 · P95<br/>Production user base"]
MK["<b>MetricKit (iOS)</b><br/>24hr diagnostic reports<br/>Launch histogram · Hang duration"]
BENCH["<b>Jetpack Macrobenchmark</b><br/>Cold start regression in CI<br/>Blocks PR on regression"]
end
STARTUP --> FRAME --> MEMORY --> MEASURE
style STARTUP fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style FRAME fill:#DCFCE7,stroke:#16A34A,stroke-width:2px
style MEMORY fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style MEASURE fill:#F3E8FF,stroke:#7C3AED,stroke-width:2px
style BP fill:#BFDBFE,stroke:#2563EB,color:#1e3a5f
style ASL fill:#BFDBFE,stroke:#2563EB,color:#1e3a5f
style INS fill:#BFDBFE,stroke:#2563EB,color:#1e3a5f
style RC fill:#BBF7D0,stroke:#16A34A,color:#14532D
style SM fill:#BBF7D0,stroke:#16A34A,color:#14532D
style GPU fill:#BBF7D0,stroke:#16A34A,color:#14532D
style LC fill:#FDE68A,stroke:#D97706,color:#78350F
style HD fill:#FDE68A,stroke:#D97706,color:#78350F
style FBP2 fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95
style MK fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95
style BENCH fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95
References
- Google — App Startup Time. developer.android.com/topic/performance/vitals/launch-time
- Google — Baseline Profiles. developer.android.com/topic/performance/baselineprofiles
- Apple — MetricKit. developer.apple.com/documentation/metrickit
- LeakCanary — Memory Leak Detection. square.github.io/leakcanary
Mobile Engineering Reference
← Mobile Development