Performance Testing of Mobile Application
App works fine on iPhone 15 Pro. On Xiaomi Redmi 10A with Android 11, which a third of audience uses, main screen loads 4 seconds, and list with fast scroll stutters down to 20 fps. Firebase Crashlytics won't show — not a crash. App Store Reviews will show after a week of release.
Performance profiling is not just running and looking. It's reproducible measurements with specific numbers, before/after comparison, root cause localization to specific method.
iOS: Xcode Instruments
Instruments is main tool on iOS. Templates we use:
Time Profiler — where CPU spends time. Run heavy scenario (scroll, screen load), see Call Tree with Invert Call Tree + Hide System Libraries. See own code with load percentages.
Core Animation (Rendering) — FPS and drop causes. Commit — layer formation time, Render — GPU time. High Commit — main thread issue. Red line at 16.67 ms (60 fps) or 8.33 ms (120 fps, ProMotion) — clear boundary.
Allocations — memory allocation patterns. Run, do action, see Generation Analysis. If memory doesn't drop after Release navigation — leak.
Real problem example: one project's photo collection screen stuttered on scroll. Time Profiler showed 23% time on UIImage(data:) in cellForItemAt. Synchronous JPEG decoding on main thread. Solution: ImageIO + kCGImageSourceShouldCacheImmediately: false + decoding on background queue with DispatchQueue.global(qos: .userInitiated). FPS rose from 35 to 58.
Startup Metrics
MetricKit (iOS 13+) collects production metrics from real user devices:
class AppMetricsObserver: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
if let launchMetrics = payload.applicationLaunchMetrics {
// resumeTime — time on background→foreground
// timeToFirstDraw — cold start
let coldStart = launchMetrics.histogrammedTimeToFirstDraw
// send to analytics
}
}
}
}
Not synthetic measurements in tests, but real data from user devices. Complements Instruments profiling.
Android: Android Profiler and Macrobenchmark
In Android Studio — Android Profiler. CPU profiler in Sample Java/Kotlin Methods mode for overview, Trace Java/Kotlin Methods for precise tracing (with overhead). System Trace — for GPU interaction, Choreographer, RenderThread.
Janky frames (>16 ms): adb shell dumpsys gfxinfo com.example.app | grep "Janky frames". More than 5% janky — issue.
Macrobenchmark — Jetpack library for reproducible measurements:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD,
) {
pressHome()
startActivityAndWait()
}
}
Runs on real device (not emulator), returns timeToInitialDisplay and timeToFullDisplay in milliseconds. Stable between runs — these are measurements, not stopwatch pressing.
Slow Rendering: Jetpack Compose
For Compose — Recomposition counter in Layout Inspector. More precise — ComposeUiTest with measureRepeated:
@Test
fun scrollPerformance() {
benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
) {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// scroll list
device.findObject(UiSelector().resourceId("com.example.myapp:id/feed_list"))
.flingForward()
}
}
FrameTimingMetric collects frame data: frameOverrunMs — how much frame exceeded budget.
Flutter: DevTools and flutter_driver
Flutter DevTools → Performance view shows Frame chart with UI thread and Raster thread. Red frames — UI thread busy longer than 16 ms. Yellow — Raster thread.
Common cause of red frames: setState() rebuilds too large subtree. Solution — const constructors where data unchanged, RepaintBoundary for isolating animated elements repaint.
// Bad: entire screen rebuilds on each timer tick
class CounterScreen extends StatefulWidget { ... }
// Better: only counter isolated
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(children: [
const HeaderWidget(), // const — not rebuilt
CounterWidget(), // only this part rebuilds
]);
}
}
What's Included
- App startup profiling (cold start, warm start)
- FPS analysis on scroll and navigation
- Memory leak search via Allocations / Memory Profiler
- CPU profile analysis on heavy operations
- Macrobenchmark tests for Android, MetricKit integration for iOS
- Report with specific before/after numbers and recommendations
Timeline
3–5 days — profiling, problem localization, report. If optimization implementation also needed — estimate separately by change volume. Cost is calculated individually.







