MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Performance testing is often skipped until users complain. By then it's expensive to fix. Here's what to measure, how to set baselines, and the minimum viable performance testing setup for any application.
On this page
Performance bugs are the worst kind to fix late. A functional bug is a logic error in one place. A performance problem often reveals an architectural decision made months ago that touched dozens of files.
The time to find performance problems is before users find them for you.
The umbrella term "performance testing" includes several distinct types:
| Type | Question It Answers |
|---|---|
| Load testing | Does it work under expected traffic? |
| Stress testing | When does it break and how does it fail? |
| Endurance testing | Does it degrade over time with sustained load? |
| Spike testing | Can it handle sudden traffic surges? |
| App performance | How fast does this screen render? How much memory? |
Mobile apps care most about app performance. Backend services care about all of them.
Startup time — how long from launch to first interactive frame:
Target cold start: < 1 second. > 5 seconds is unacceptable.
Frame rate — should maintain 60fps (16ms per frame), 90/120fps on high-refresh displays. Jank (frame drops) is visible and users notice it.
Memory usage — continuous growth over time indicates a memory leak. Spikes indicate inefficient object creation.
Battery impact — apps that drain the battery get uninstalled.
Network efficiency — unnecessary API calls, large response payloads, no caching.
Android provides built-in startup measurement:
# Cold start time (kills app first)
adb shell am force-stop com.yourapp.package
adb shell am start-activity -S -W com.yourapp.package/.MainActivity
# Output:
# ThisTime: 312
# TotalTime: 847
# WaitTime: 862TotalTimeFor automated measurement in your pipeline:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.yourapp.package",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
}This uses the Macrobenchmark library to measure startup across multiple iterations.
@Test
fun scrolling_performance() = benchmarkRule.measureRepeated(
packageName = "com.yourapp.package",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
startupMode = StartupMode.WARM
) {
startActivityAndWait()
val listView = device.findObject(By.res("task_list"))
listView.fling(Direction.DOWN)
}This measures frame times during scrolling. You'll see P50, P90, P95 frame durations. P95 < 16ms = smooth. P95 > 32ms = noticeable jank.
LeakCanary is the standard tool for Android memory leak detection:
// build.gradle.kts
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.13")
}No code changes needed. LeakCanary automatically monitors Activity and Fragment lifecycle. When a leak is detected, it shows a notification with a full stack trace showing what's holding the reference.
Run through your app's main flows during development. Any leak appears immediately.
For API/backend performance testing, k6 is the modern choice:
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50, // 50 virtual users
duration: '30s', // for 30 seconds
thresholds: {
http_req_duration: ['p(95)<200'], // 95th percentile < 200ms
http_req_failed: ['rate<0.01'], // < 1% error rate
},
};
export default function () {
const res = http.get('https://api.yourapp.com/tasks');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(1);
}Run it:
k6 run load-test.jsIf the 95th percentile exceeds your threshold, the test fails. Add this to CI for regression detection.
The most important performance testing principle: measure before you optimize.
Establish baselines for your app's current performance:
Write these down. Now you can detect regressions. If a PR changes your cold start from 847ms to 1,400ms, that's visible in CI.
Main thread work. Disk I/O, network calls, or heavy computation on the main thread causes ANRs and jank. Move to coroutines with
Dispatchers.IOLarge images. Loading a 4K image into a 200x200 ImageView wastes memory. Use Coil with proper resizing:
AsyncImage(
model = ImageRequest.Builder(context)
.data(imageUrl)
.size(200, 200) // Resize before loading
.build(),
contentDescription = null
)Unnecessary recompositions. In Compose, reading a State inside a composable triggers recomposition on every change. Be intentional about which composables observe which state.
Deep object hierarchies in RecyclerView/LazyColumn. Flatten your data structures. Complex nested ViewHolders are slow to bind.
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus