Recomposition Performance
Stability, derivedStateOf & key/remember
Slow Compose UIs almost always come from recomposing too much. Understanding stable types, deferring reads, and deriving state lets you cut wasted work without changing what the user sees.
📚 Explanation
Compose skips a composable when all its inputs are `stable` and unchanged. Marking models `@Immutable` (or using `kotlinx.collections.immutable`) tells the compiler a value can't change underneath it, enabling skipping. `derivedStateOf` is the key tool for high-frequency sources: reading `firstVisibleItemIndex` directly would recompose every scroll frame, but wrapping a coarse condition in `derivedStateOf` only triggers when the *result* changes. Stable `key`s in `items {}` let Compose match items across list updates instead of re-laying-out everything.
Note: Code execution is not available for Android/Kotlin in the browser. Use Android Studio to run and test the app.
import androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.itemsimport androidx.compose.foundation.lazy.rememberLazyListStateimport androidx.compose.runtime.*// 1. Stable, immutable models recompose less.// A List<T> is "unstable"; an ImmutableList is stable.@Immutabledata class FeedItem(val id: Long, val title: String)@Composablefun Feed(items: List<FeedItem>, onClick: (Long) -> Unit) {val listState = rememberLazyListState()// 2. derivedStateOf: recompute ONLY when the boolean result flips,// not on every pixel of scroll.val showScrollToTop by remember {derivedStateOf { listState.firstVisibleItemIndex > 5 }}LazyColumn(state = listState) {// 3. A stable key lets Compose reuse slots across updatesitems(items, key = { it.id }) { item ->Row(item = item, onClick = onClick)}}if (showScrollToTop) ScrollToTopButton()}// 4. Passing a lambda that doesn't capture changing state stays stable@Composableprivate fun Row(item: FeedItem, onClick: (Long) -> Unit) {Text(item.title, modifier = Modifier.clickable { onClick(item.id) })}