Production engineering guide for mobile performance profiling covering patterns, implementation strategies, and operational best practices.
Mobile Performance Profiling is the process of measuring, analysing, and diagnosing the responsiveness, memory usage, CPU efficiency, and network behaviour of a mobile application across real devices and network conditions. It matters when users report lag, app crashes, or slow startup, and when performance metrics deviate from expected baselines—particularly during feature rollouts, app updates, or changes to backend APIs.
Profile the app during real user workflows: cold startup, scrolling through a feed, loading a complex screen, and handling background tasks. Use device-level tools to capture data across both CPU and memory, and correlate events with user interactions.
Use systrace from Android SDK to generate a timeline of system and app-level events.
adb shell am start -W -n com.example.app/.MainActivity
adb shell am profile start --time 30 --output /sdcard/perf-trace.trace com.example.app
sleep 30
adb shell am profile stop com.example.app
adb pull /sdcard/perf-trace.trace ./perf-trace.trace
Open the .trace file in the Chrome DevTools, where events appear as stacked bars: frame, render, input, GC, JNI, ViewRootImpl, Activity.onCreate, Activity.onResume.
frame and render indicates a dropped frame.ViewRootImpl takes longer than 16ms, but render is under 10ms—indicating a layout pass that did not complete in time.am profile start captures only the main process, but com.example.app includes multiple processes; use --processes to include :service, :worker, etc.Use Instruments via command line with xcrun.
# Launch app and record a trace for 30 seconds
xcrun instruments -t "Time Profiler" -D ./perf-instrumentation.trace \
-k "com.example.app" -w "iPhone 15 Pro" \
-e "Launch" --launch-args "com.example.app" \
--time 30 --wait-for-app
After capture, open the .trace file in Instruments or export to .csv for analysis.
Time Profiler instrument records CPU samples at 1ms intervals, but Core Animation and Core Graphics layers are sampled independently. A mismatch between Core Animation and Render times often points to off-main-thread rendering.Render time is high, but Core Animation time is low—suggesting that the app is doing work in the drawRect: method but not updating the CALayer hierarchy efficiently.Startup time is a key signal of user perception. Profile startup from cold boot to first renderable frame.
Use adb shell am to measure time from boot to first frame.
# Boot the app and measure time to first frame
adb shell am start -S -W -n com.example.app/.MainActivity
Output includes:
Total: 2345 ms
Startup: 1823 ms
Launch: 1823 ms
Application.onCreate() to the first onResume().Activity.onCreate() to Activity.onResume().am start -S triggers a cold start, but if the app is already running, it does a warm start. Use am force-stop to ensure coldness.Use systrace to correlate startup with ViewRootImpl, WindowManager, and View.measure().
Use Xcode’s Time Profiler with Instruments to capture time from main() to didFinishLaunching.
main() to didFinishLaunching is app launch time.AppDelegate sets up a UIWindow but UIView layout is done on the main thread, causing viewDidLoad to block didFinishLaunching.Track memory usage over time, especially during user interactions and long-lived sessions.
Use adb shell dumpsys meminfo and adb shell am dumpheap.
# Dump heap at current state
adb shell am dumpheap com.example.app /sdcard/app.hprof
adb pull /sdcard/app.hprof ./app.hprof
# Analyze with MAT (Memory Analyzer Tool)
# Or use CLI tool `hprof-conv` from Android SDK
hprof-conv app.hprof app-conv.hprof
In the .hprof file:
Heap dump shows Allocated, Retained, and Shallow sizes.Retained size of a List<Fragment> is 3x Shallow size—indicating that each Fragment holds references to large View trees.dumpheap includes only the current process, but Fragment objects are held across Activity lifecycles; use --full flag to capture all memory.Use Instruments with Allocations and VM Tracker instruments.
# Start recording with Instruments
xcrun instruments -t "Allocations" -D ./memory.allocations.trace \
-k "com.example.app" -w "iPhone 15 Pro" \
--time 60 --wait-for-app
After recording:
Live Bytes vs Total Bytes over time.UIViewController retains a UITableView with UITableViewCell objects, but UITableViewCell instances are not reused—leading to high Live Bytes and frequent dequeueReusableCell.Measure network requests and their impact on app responsiveness.
Use tshark to capture HTTP traffic from device to server.
# Capture all HTTP traffic on device
sudo tshark -i en0 -f "host 192.168.1.10" -Y "http" -T fields \
-e frame.time -e http.request.method -e http.request.uri \
-e http.response.code -e http.content_length -e tcp.analysis.ack_rtt \
-e http.response.time -E separator=, > network.csv
http.response.time is the time from request start to first byte.tcp.analysis.ack_rtt shows increasing RTT over time—indicating poor TCP congestion control or network instability.tshark captures all packets, but http.request.uri is missing for HTTP/2 streams—use http2 filter.curlUse curl with --write-out to collect detailed timing metrics.
curl -v -w '
time_namelookup: %{time_namelookup}s
time_connect: %{time_connect}s
time_appconnect: %{time_appconnect}s
time_pretransfer: %{time_pretransfer}s
time_starttransfer: %{time_starttransfer}s
time_total: %{time_total}s
size_download: %{size_download} bytes
speed_download: %{speed_download} bytes/s
' \
-H "Authorization: Bearer $TOKEN" \
https://api.example.com/v1/feed \
-o feed.json
Output includes:
time_namelookup: 0.150s
time_connect: 0.230s
time_appconnect: 0.310s
time_pretransfer: 0.420s
time_starttransfer: 0.680s
time_total: 1.250s
size_download: 143200 bytes
speed_download: 114560 bytes/s
time_appconnect includes TLS handshake and server application layer setup—high values indicate slow server-side processing.time_starttransfer is high but time_total is low—suggesting that the server sends data quickly but the client does not consume it efficiently.Performance issues often span app, network, and backend. Correlate events across devices and services.
opentelemetry to Instrument Mobile App and BackendExport traces using OpenTelemetry protocol.
{
"name": "app.startup",
"kind": "SpanKind.INTERNAL",
"startTime": "2025-04-05T10:00:00.000Z",
"endTime": "2025-04-05T10:00:03.500Z",
"attributes": {
"app.version": "2.3.1",
"device.model": "iPhone 15 Pro",
"network.type": "wifi",
"memory.total": 4096,
"memory.free": 2048
},
"events": [
{
"name": "frame.rendered",
"timestamp": "2025-04-05T10:00:01.200Z",
"attributes": {
"frame.time": 15.4,
"frame.dropped": true
}
}
]
}
Send to backend via curl:
curl -X POST -H "Content-Type: application/json" \
-d @./traces.json \
https://collector.example.com/v1/traces
startTime and endTime are in ISO 8601, but timestamp in events uses Unix timestamp—leading to misaligned timelines.attributes are not nested properly; app.version appears as app_version due to camelCase mismatch.systrace shows ViewRootImpl blocks onDraw, dispatchDraw, or onMeasure.AsyncTask, WorkManager, or Coroutines.OutOfMemoryError.hprof shows List<Bitmap> grows with Bitmap objects retained by ImageView.BitmapFactory.Options.inSampleSize, recycle(), and BitmapRegionDecoder.curl shows time_pretransfer > time_starttransfer—indicating slow server processing.pprof, flamegraph, or eBPF.systrace or Instruments for full timeline.adb am start -S, curl, tshark.This workflow ensures that every performance issue is not just observed, but rooted in measurable data across device, app, and network layers.
This page was rewritten on 18 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