# Compose Performance

> Jetpack Compose performance optimization patterns for this project. Covers recomposition rules, type stability with @Immutable/@Stable, lambda stability for callbacks, deferred state reads, PagingData with stable item keys, animation performance, and debugging recomposition with Layout Inspector.

- Skill: `thetruong1099/compose-performance` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thetruong1099/compose-performance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thetruong1099/compose-performance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: thetruong1099 (https://skillmd.com/u/thetruong1099)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thetruong1099/compose-performance

---


# Compose Performance

## Recomposition Rules

1. Composable functions can recompose at any time — no side effects in body
2. Recomposition skips unchanged parameters — make parameters stable
3. Use `remember` for expensive computations
4. Use `derivedStateOf` for derived state

## Stability

### What makes a type stable?

- Primitives (`Int`, `String`, `Boolean`)
- `@Immutable` or `@Stable` annotated classes
- `data class` with all stable properties

```kotlin
// Domain models passed to Composable should be stable
@Immutable
data class ItemUiModel(val id: String, val name: String, val avatarUrl: String)

// ViewState: data class is stable if all fields are stable
// Note: Flow fields cause instability but are acceptable for PagingData
data class SampleViewState(
    val items: Flow<PagingData<ItemUiModel>>? = null,  // Flow is NOT stable, but OK here
) : IViewState
```

## Lambda Stability

```kotlin
// GOOD: Lambda hoisted to stateful Screen - created once, passed down
SampleScreenInternal(
    onItemClick = { item -> viewModel.onTriggerEvent(SampleViewEvent.OnItemClick(item)) }
)

// BAD: Lambda created inline causes recomposition
ItemCard(
    onItemClick = { viewModel.onTriggerEvent(SampleViewEvent.OnItemClick(it)) }
    // New lambda instance every recomposition!
)
```

## Defer State Reads

```kotlin
// BAD: Reading state in composition phase
Box(modifier = Modifier.offset(y = scrollState.value.dp))

// GOOD: Defer read to layout phase with lambda
Box(modifier = Modifier.offset { IntOffset(0, scrollState.value) })
```

## remember & derivedStateOf

```kotlin
val sortedItems = remember(items) { items.sortedBy { it.name } }
val hasItems by remember { derivedStateOf { items.isNotEmpty() } }
```

## PagingData + Compose (Correct Pattern)

```kotlin
@Composable
fun SampleScreenInternal(viewModel: IBaseViewModel<...>) {
    val refreshState = rememberPagingRefreshState<ItemModel>()

    BaseScreenComponent(
        viewModel = viewModel,
        externalIsRefreshing = refreshState.isRefreshing,
        onRefreshListener = { refreshState.refresh() },
    ) { state, padding ->
        val items = state.items?.collectAsLazyPagingItems()  // lifecycle-aware
        refreshState.bind(items)

        PagingVerticalGridComponent(
            items = items,
            columns = GridCells.Fixed(2),
        ) { pagingItems ->
            items(
                count = pagingItems.itemCount,
                key = pagingItems.itemKey { it.id },  // Stable keys - required!
            ) { index ->
                pagingItems[index]?.let { ItemCard(it) }
            }
        }
    }
}
```

## Animation Performance

```kotlin
// Use Compose animation APIs
val alpha by animateFloatAsState(
    targetValue = if (isVisible) 1f else 0f,
    animationSpec = tween(300),
    label = "alpha",
)

// Transition API for complex animations
val transition = updateTransition(targetState = isExpanded, label = "expand")
val height by transition.animateDp(label = "height") { expanded ->
    if (expanded) 200.dp else 56.dp
}
```

## Debugging Recomposition

Use Android Studio Layout Inspector with "Show Recomposition Counts" enabled.

```kotlin
// Debug-only: recomposition highlighter
@Composable
fun Modifier.recompositionHighlighter(): Modifier {
    val count = remember { mutableIntStateOf(0) }
    count.intValue++
    return this.drawWithContent {
        drawContent()
        drawRect(
            color = Color.Red.copy(alpha = (count.intValue * 0.1f).coerceAtMost(1f)),
            size = Size(4.dp.toPx(), size.height),
        )
    }
}
```

