Optimizing Android App Performance
Android app performance directly impacts user retention, battery life, and app store ratings. Performance optimization covers three main areas: rendering (smooth 60/120fps UI), memory (avoiding leaks and excessive allocation), and battery (minimizing wake locks, network calls, and background work). This article covers profiling tools and optimization techniques for production Android apps.
Rendering Performance with the Profile GPU tool
The Android GPU Profiling overlay (Developer Options → Profile GPU rendering) shows a bar chart of how long each frame takes to render. If bars consistently exceed 16ms (for 60fps) or 8ms (for 120fps), the UI is janky. Common causes include: complex layouts with too many nested views, expensive draw operations (large bitmaps, complex shadows), and main thread blocking from disk I/O or network calls. Jetpack Compose’s layout inspector shows recomposition counts—excessive recomposition is a sign that state is being read at the wrong scope.
// Fix: Move state reads to the correct scope
@Composable
fun MyScreen() {
// BAD: Reading state here recomposes the entire screen
val scrollState = rememberScrollState()
// GOOD: Use derivedStateOf for expensive computations
val isAtBottom by remember {
derivedStateOf { scrollState.value >= scrollState.maxValue }
}
LazyColumn(state = scrollState) {
items(100) { index ->
// Item content only recomposes when its own data changes
ListItem(index = index)
}
}
}
Memory Management and Leak Detection
Android has limited memory, and the system kills apps that exceed their allocation. Memory leaks occur when an object holds a reference to an Activity or Context after the Activity is destroyed—common causes include anonymous inner classes, static references to Views, and unregistered listeners. LeakCanary is a memory leak detection library that automatically detects and reports leaks with the reference chain. Profile with Android Studio’s Memory Profiler: look for objects that should be garbage collected but aren’t, and use MAT (Memory Analyzer Tool) to analyze heap dumps for suspected leaks.
// Common leak pattern
class MyActivity : AppCompatActivity() {
// BAD: Static reference to Activity context
companion object {
var sCallback: MyCallback? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sCallback = MyCallback() // Leaks activity on rotation!
}
}
// FIX: Use application context or lifecycle-aware callbacks
class MyApplication : Application() {
var callback: MyCallback? = null // Application lives forever, no leak
}
Battery Optimization
Network requests are the #1 battery drain. Batch them using WorkManager for deferrable work, and use exponential backoff for retries. Reduce wake locks by using PendingIntent with FLAG_UPDATE_CURRENT instead of keeping the CPU awake. For location updates, use fused location provider with appropriate priority and interval rather than GPS directly. Background work should use WorkManager with constraints (network availability, idle status) rather than foreground services unless absolutely necessary.
// Efficient background work with WorkManager
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(uploadWork)
Android Studio’s Energy Profiler visualizes power usage by component (CPU, network, GPS, wake locks). Profile on a physical device rather than an emulator for accurate battery readings, and always test on low-end devices (e.g., a 2019 Android Go phone) to ensure your app performs acceptably for the widest possible audience. Performance optimization is a continuous process—integrate benchmarking into your CI pipeline with Android Benchmark library and track regressions over time.
Startup Time Optimization
App startup time is critical for user retention—every 100ms delay reduces conversion by 1%. Android measures startup as cold (process created from scratch), warm (Activity recreated from cached process), and hot (Activity brought to foreground). The most impactful startup optimizations: defer non-critical initialization (lazy init with App Startup library), reduce content provider initialization (merge providers with CombineProviders), use baseline profiles (pre-compiled code paths shipped with the APK), and apply startup best practices from the Android Vitals dashboard. App Startup library initializes components on-demand rather than at application start, ensuring only the components needed for the current screen are loaded. Baseline profiles (generated with Jetpack Macrobenchmark) tell ART (Android Runtime) which code paths to ahead-of-time compile, reducing just-in-time compilation overhead on first launch.
// App Startup: defer non-critical initialization
@InitializationBug
class Initializer : Initializer<Unit> {
override fun create(context: Context) {
// Non-critical init moved here instead of Application.onCreate()
Analytics.initialize(context)
}
override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}
Network Optimization
Network requests are often the performance bottleneck in Android apps. Reduce network latency through: HTTP/2 multiplexing (single connection for multiple concurrent requests), response compression (gzip/deflate), client-side caching with OkHttp cache, optimistic UI updates (show results immediately while network requests run in background), and image caching with Coil or Glide (memory + disk cache). For API responses, pagination with cursor-based pagination limits payload size. GraphQL over REST reduces over-fetching. Use OkHttp’s event listener to monitor request timings (DNS lookup, TCP connect, TLS handshake, response body download) and identify bottlenecks.

