| name | performance |
| description | Android app performance for AI agents. Use this skill whenever optimizing app performance,
reducing recomposition, Compose stability, @Stable, @Immutable, skippable composables,
baseline profiles, R8 optimization, lazy loading, image memory, Coil caching, RecyclerView
vs LazyList performance, memory leaks, LeakCanary, ANR prevention, background work,
Trace API, systrace, profiling, overdraw, rendering performance, startup time optimization,
or any Android performance concern. Apply whenever a screen feels slow or laggy.
|
Android Performance
Rule 1: Compose stability — stop unnecessary recomposition
@Stable
class ItemState(
val id: String,
val title: String,
var isExpanded: Boolean = false
)
@Immutable
data class Item(
val id: String,
val title: String,
val tags: List<String>
)
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
@Composable
fun ItemList(items: ImmutableList<Item>) {
LazyColumn {
items(items, key = { it.id }) { item ->
ItemCard(item = item)
}
}
}
@Composable
fun ItemList(items: List<Item>) { ... }
Rule 2: derivedStateOf — compute only when dependencies change
@Composable
fun ItemList(items: List<Item>) {
val listState = rememberLazyListState()
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 3 }
}
val activeItems by remember(items) {
derivedStateOf { items.filter { it.isActive } }
}
Box {
LazyColumn(state = listState) {
items(activeItems, key = { it.id }) { ItemCard(it) }
}
AnimatedVisibility(visible = showScrollToTop, modifier = Modifier.align(Alignment.BottomEnd)) {
ScrollToTopButton(onClick = { })
}
}
}
Rule 3: Baseline Profiles — fast startup
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() {
rule.collect(packageName = "com.company.app") {
pressHome()
startActivityAndWait()
device.findObject(By.text("Home")).click()
device.waitForIdle()
}
}
}
dependencies {
implementation(libs.profileinstaller)
}
Rule 4: R8 — enable shrinking in release
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
# proguard-rules.pro — keep what R8 would remove incorrectly
-keep class com.company.app.data.remote.dto.** { *; } # keep serialization DTOs
-keepclassmembers class * { @com.google.gson.annotations.SerializedName <fields>; }
# Kotlin
-keep class kotlin.** { *; }
-keepclassmembers class **$WhenMappings { *; }
Rule 5: Image loading performance
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.size(Size.ORIGINAL)
.crossfade(true)
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
)
val imageLoader = LocalContext.current.imageLoader
LaunchedEffect(items) {
items.take(3).forEach { item ->
imageLoader.enqueue(
ImageRequest.Builder(context).data(item.imageUrl).build()
)
}
}
Rule 6: Main thread protection
suspend fun processData(input: List<Item>): List<ProcessedItem> = withContext(Dispatchers.Default) {
trace("processData") {
input.map { processItem(it) }
}
}
val item = runBlocking { itemDao.getById(id) }
val item = withContext(Dispatchers.IO) { itemDao.getById(id) }
Common Mistakes
❌ Passing List<T> to Composable — use ImmutableList<T> or wrap in @Immutable class
❌ Computing in composition — move to remember {} or derivedStateOf {}
❌ isMinifyEnabled = false in release — leaves dead code in APK
❌ Loading full-resolution images — always size images to display size
❌ Running DB queries on Main thread — always withContext(Dispatchers.IO)
❌ No baseline profile — first launch is 40-60% slower without it