Skip to content
All posts
March 3, 20264 min read

Performance Testing Basics: What to Measure and How to Start

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.

TestingPerformanceAndroid
Share:

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.


What Performance Testing Actually Covers

The umbrella term "performance testing" includes several distinct types:

TypeQuestion It Answers
Load testingDoes it work under expected traffic?
Stress testingWhen does it break and how does it fail?
Endurance testingDoes it degrade over time with sustained load?
Spike testingCan it handle sudden traffic surges?
App performanceHow fast does this screen render? How much memory?

Mobile apps care most about app performance. Backend services care about all of them.


For Android Apps: What to Measure

Startup time — how long from launch to first interactive frame:

  • Cold start: app not in memory
  • Warm start: app in memory, activity destroyed
  • Hot start: app in memory, activity in back stack

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.


Measuring Startup Time

Android provides built-in startup measurement:

bash
# 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: 862

code
TotalTime
is the most useful — it measures from launch request to Activity.onResume().

For automated measurement in your pipeline:

kotlin
@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.


Measuring Frame Rendering

kotlin
@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.


Detecting Memory Leaks

LeakCanary is the standard tool for Android memory leak detection:

kotlin
// 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.


Backend Performance: Getting Started With k6

For API/backend performance testing, k6 is the modern choice:

javascript
// 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:

bash
k6 run load-test.js

If the 95th percentile exceeds your threshold, the test fails. Add this to CI for regression detection.


Setting Performance Baselines

The most important performance testing principle: measure before you optimize.

Establish baselines for your app's current performance:

  1. Cold start: 847ms
  2. Screen load (main screen): 312ms
  3. API call (task list): P95 = 180ms
  4. Memory (after 10 min use): 85MB
  5. Battery drain per hour: 3%

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.


Common Android Performance Mistakes

Main thread work. Disk I/O, network calls, or heavy computation on the main thread causes ANRs and jank. Move to coroutines with

code
Dispatchers.IO
.

Large images. Loading a 4K image into a 200x200 ImageView wastes memory. Use Coil with proper resizing:

kotlin
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.


Takeaways

  • Establish baselines before optimizing — you can't detect regression without them
  • Measure startup time with Macrobenchmark and track it in CI
  • Use LeakCanary in debug builds — it catches leaks automatically
  • Keep main thread strictly free of I/O and heavy computation
  • Backend performance testing with k6 is a 30-minute setup with immediate value
Share:
S

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.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

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.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus