Skip to main content
Module: 10 of 13Duration: 2 weeksTopics: 2 · 4 subtopicsPrerequisites: Modules 01–09

Performance Optimization

A slow app feels broken. A janky scroll feels broken. A 2-second startup feels broken. This module teaches you to measure first, then optimize the things that matter — using the same tools the Android team uses internally.

Topic 1 · Profiling

Memory management & LeakCanary

The JVM garbage collector frees objects no longer reachable. A leak occurs when something keeps a reference longer than it should — preventing GC. Common culprits on Android:

  • Inner classes / lambdas holding the outer Activity
  • Static references to Context
  • Listeners not unregistered
  • Coroutines launched on the wrong scope

LeakCanary catches leaks automatically in debug builds:

// app/build.gradle.kts
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")

When LeakCanary detects a leaked Activity/Fragment/View, it dumps the heap, shows the leak trace, and points at the exact reference holding the object. Fix leaks immediately — they compound and cause OOM crashes in the wild.

Layout performance

In Compose, the main perf risks are:

  • Excessive recomposition — composables recomposing when nothing visible changed
  • Lazy lists without keys — items recompose on reorder
  • Heavy work in composition — read DB, do network, parse JSON in a @Composable
// BAD — recomposes on EVERY parent recomposition because mutableListOf creates a new list
@Composable
fun BadList() {
val items = mutableListOf(1, 2, 3) // ← new instance every recomposition
LazyColumn { items(items) { Text("$it") } }
}

// GOOD — remember keeps the same instance, with a stable key
@Composable
fun GoodList() {
val items = remember { listOf(1, 2, 3) }
LazyColumn { items(items, key = { it }) { Text("$it") } }
}

Enable Layout Inspector → Recomposition counts in Android Studio to spot hot composables.

For XML layouts: prefer ConstraintLayout (flat hierarchies), avoid nested RelativeLayout/LinearLayout, and watch the GPU profiler for overdraw.

App startup optimization

Startup has three phases the platform measures:

PhaseWhat happens
Cold startProcess create, Application.onCreate, first frame
Warm startActivity recreated, no process create
Hot startActivity already in memory, brought to front

To improve cold start:

  • Defer initialization with App Startup library instead of running everything in Application.onCreate.
  • Avoid heavy DI graph eager init (Hilt is already lazy).
  • Use SplashScreen API (Android 12+) — the system shows the splash for free.
  • Move first-frame work to Compose's LaunchedEffect so it doesn't block layout.
// App Startup — declarative, lazy initialization
class AnalyticsInitializer : Initializer<AnalyticsClient> {
override fun create(ctx: Context): AnalyticsClient = AnalyticsClient(ctx)
override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}
<!-- AndroidManifest.xml -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.AnalyticsInitializer"
android:value="androidx.startup" />
</provider>

Topic 2 · Optimization

R8 / ProGuard — code shrinking, obfuscation, optimization

R8 (Google's replacement for ProGuard) does three things at release build:

  1. Shrinking — removes unused classes, methods, fields.
  2. Optimization — inlines methods, removes dead code, unboxes primitives.
  3. Obfuscation — renames classes/methods to short names (a, b, c) to shrink and obscure.
// app/build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}

You'll need keep rules for code accessed via reflection (e.g., Retrofit interfaces, Moshi-generated adapters, custom Compose @Stable types):

# proguard-rules.pro
-keepattributes *Annotation*, Signature, InnerClasses

# Retrofit
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation

# Moshi
-keep class **JsonAdapter { *; }

APK size reduction

TechniqueTypical savings
Enable R8 with shrinkResources30–50%
Use App Bundles (.aab)15–25% per device download
WebP / AVIF for images25–35% vs PNG
Vector drawables for icons100x vs raster at all densities
Remove unused languages with resConfigsa few hundred KB
android {
defaultConfig {
resourceConfigurations += listOf("en", "hi", "es", "fr") // ship only these locales
}
bundle {
language { enableSplit = true } // per-language splits
density { enableSplit = true }
abi { enableSplit = true }
}
}

Baseline profiles

Baseline profiles ship with your APK telling ART which methods to AOT-compile at install time. The result: 20–40% faster startup and smoother first launches of critical screens.

// macrobenchmark module — generate the profile
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule val rule = MacrobenchmarkRule()

@Test fun startup() = rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
}

@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule val rule = BaselineProfileRule()

@Test fun generate() = rule.collect(packageName = "com.example.app") {
pressHome()
startActivityAndWait()
// walk through critical user flows here
}
}

The generated baseline-prof.txt ships in src/main/ and is consumed automatically by the Android build system.

Other tools you'll use

📊

Android Studio CPU Profiler

Sample method traces, see where time is spent on the main thread.

🧠

Memory Profiler

Track allocations, find retained objects, force GC, dump heaps.

🌡️

Energy Profiler

See CPU, network, and location wakelocks combined into a battery score.

🔍

Perfetto traces

Microsecond-level system traces — the same tool Google uses internally.

⏱️

Macrobenchmark

Measure startup time, frame timing, and scrolling on real devices in CI.

🪶

Firebase Performance

Production trace collection — see real-user metrics from real devices.


Key takeaways

Practice exercises

  1. 01

    Add LeakCanary

    Install LeakCanary in your debug build and intentionally create a leak (static reference to an Activity). Observe the leak trace.

  2. 02

    Measure recomposition

    Use Layout Inspector in Compose mode to find a composable recomposing too often, then fix it with derivedStateOf or remember.

  3. 03

    Generate baseline profile

    Set up the macrobenchmark module and generate a baseline profile for your app's startup flow.

Next module

Continue to Module 11 — Google Play Store & Publishing to ship the optimized app to users.