mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 18:39:55 +02:00
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
167 lines
7.5 KiB
Markdown
167 lines
7.5 KiB
Markdown
# Performance & Recomposition
|
|
|
|
## Three Phases and Primitive Specializations
|
|
|
|
Compose executes Composition, Layout, and Drawing phases per frame. State reads in later phases skip earlier phases — moving reads from Composition to Layout/Drawing eliminates recomposition for those reads. Use `Modifier.offset { }` (lambda) instead of `Modifier.offset()`. Use `mutableIntStateOf()`/`mutableFloatStateOf()` instead of `mutableStateOf<Int>()` to avoid boxing. See [compose-essentials.md](compose-essentials.md) for full explanation and code examples.
|
|
|
|
## Performance Mistakes and Fixes
|
|
|
|
| # | Issue | Fix |
|
|
|---|---|---|
|
|
| 1 | Unstable parameters (`MutableList`, lambdas in state models, anonymous objects) | Immutable data classes + immutable collections |
|
|
| 2 | Broad state observation — parent reads whole state, ripples through tree | Collect once at route, slice aggressively for leaves |
|
|
| 3 | Large state passed everywhere — many nodes observe unused fields | Pass only what each child renders |
|
|
| 4 | Callback recreation in hot paths (large lazy lists, nested rows) | `remember(key, callback)` for repeated rows |
|
|
| 5 | Expensive calculations during composition (parse, sort, filter, format) | Move upstream to ViewModel/domain |
|
|
| 6 | `remember` misuse — caching business state, hiding architecture issues | Use only for local UI state, expensive local objects, hot callback adaptation |
|
|
| 7 | `derivedStateOf` misuse — wrapping cheap expressions | Use only when derived from rapidly changing Compose state with coarse output |
|
|
| 8 | `rememberSaveable` misuse — entire screen state, large graphs | Use only for tiny UI-local values surviving recreation |
|
|
| 9 | State reads too high in tree (`LazyListState`, animation, keyboard state) | Read close to use |
|
|
| 10 | List recomposition — missing keys, unstable items, inline filters/sorts | Stable keys, immutable models, pre-computed data |
|
|
| 11 | Reducer emits excessive updates — same state, rebuilds on every keystroke | Guard identical transitions, emit only on semantic change |
|
|
| 12 | Ephemeral visual state in global screen state (shimmer alpha, pulse phase) | Keep visual-only state local |
|
|
| 13 | Equality pitfalls — lambdas in data classes, random IDs, mutable collections | No lambdas/mutables in data classes, stable IDs |
|
|
| 14 | Abusing `@Immutable`/`@Stable` to silence compiler | Use only to describe truth — `@Immutable` for truly immutable, `@Stable` rare in app code |
|
|
| 15 | Raw text input in MVI causing stutter (25+ fields) | `TextFieldState`/`BasicTextField2`, group fields into nested data classes, isolate read scopes |
|
|
| 16 | State reads in Composition phase for layout/draw values | Lambda modifiers: `Modifier.offset { IntOffset(scrollOffset, 0) }` |
|
|
|
|
## API Decision Table
|
|
|
|
| API | Use it for | Do not use it for |
|
|
|---|---|---|
|
|
| `remember` | local objects/state across recompositions | business state, repo results, derived domain data |
|
|
| `rememberSaveable` | small UI-local state needing restoration | whole screen state, large graphs, domain objects |
|
|
| `derivedStateOf` | reducing downstream updates from fast-changing Compose state | cheap string concatenation, reducer-owned derivations |
|
|
| `key` | preserving identity in dynamic children/lists | hiding bad state models |
|
|
| `LaunchedEffect` | collecting UI effects, startup event, one-shot route work | screen business logic in leaves |
|
|
| `DisposableEffect` | register/unregister listeners with cleanup | long-running business jobs |
|
|
| `produceState` | bridging external async/callback source to local Compose state | replacing a real ViewModel |
|
|
| `snapshotFlow` | turning Compose state reads into `Flow` operators | normal state rendering |
|
|
| `collectAsState` | collect `StateFlow` into Compose | collecting everywhere in the tree |
|
|
| lifecycle-aware collection | Lifecycle host integration (multiplatform since lifecycle 2.8+) | common leaf components |
|
|
| stable callbacks | hot repeated UI paths | every single callback everywhere |
|
|
|
|
## Code Examples
|
|
|
|
### BAD: calculating derived results in a composable
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun CalculatorResult(state: CalculatorState) {
|
|
val area = state.input.areaText.toDoubleOrNull() ?: 0.0
|
|
val materialRate = state.input.materialRateText.toDoubleOrNull() ?: 0.0
|
|
val subtotal = (area * materialRate)
|
|
Text("Subtotal: $subtotal")
|
|
}
|
|
```
|
|
|
|
### GOOD: derive upstream, narrow reads
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun CalculatorScreen(state: CalculatorState, onEvent: (CalculatorEvent) -> Unit) {
|
|
Header(title = "Estimator")
|
|
CalculatorForm(
|
|
input = state.input,
|
|
validation = state.validation,
|
|
enabled = !state.isRefreshingQuote,
|
|
onAreaChanged = { onEvent(CalculatorEvent.FieldChanged(FormField.Area, it)) },
|
|
)
|
|
ResultCard(derived = state.derived, isRefreshing = state.isRefreshingQuote)
|
|
}
|
|
|
|
@Composable
|
|
fun CalculatorResult(derived: CalculatorDerived?) {
|
|
Text(text = derived?.subtotal?.toString() ?: "—")
|
|
}
|
|
```
|
|
|
|
### BAD: unstable list items
|
|
|
|
```kotlin
|
|
data class HistoryRowState(
|
|
val id: String, val title: String,
|
|
val tags: MutableList<String>, // unstable
|
|
val onClick: () -> Unit, // lambda in data class
|
|
)
|
|
```
|
|
|
|
### GOOD: immutable models, stable keys, callback stability
|
|
|
|
```kotlin
|
|
@Immutable
|
|
data class HistoryRowUi(val id: String, val title: String, val subtitle: String)
|
|
|
|
@Composable
|
|
fun HistoryList(items: ImmutableList<HistoryRowUi>, onOpen: (String) -> Unit) {
|
|
LazyColumn {
|
|
items(items = items, key = { it.id }) { item ->
|
|
val onClick = remember(item.id, onOpen) { { onOpen(item.id) } }
|
|
ListItem(
|
|
headlineContent = { Text(item.title) },
|
|
supportingContent = { Text(item.subtitle) },
|
|
modifier = Modifier.clickable(onClick = onClick),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### GOOD: correct `derivedStateOf`
|
|
|
|
```kotlin
|
|
val listState = rememberLazyListState()
|
|
val showScrollToTop by remember { derivedStateOf { listState.firstVisibleItemIndex > 2 } }
|
|
```
|
|
|
|
### BAD: unnecessary `derivedStateOf`
|
|
|
|
```kotlin
|
|
val text by remember { derivedStateOf { if (canSubmit) "Submit" else "Fix errors" } }
|
|
// Just write: Text(if (canSubmit) "Submit" else "Fix errors")
|
|
```
|
|
|
|
### GOOD: guard identical transitions
|
|
|
|
```kotlin
|
|
private fun onAreaEdited(raw: String) {
|
|
val old = _state.value
|
|
if (old.input.areaText == raw) return
|
|
_state.value = old.copy(input = old.input.copy(areaText = raw))
|
|
}
|
|
```
|
|
|
|
## Compiler and Build Optimizations
|
|
|
|
- **Strong Skipping Mode** — enable via compiler flags; allows composables with unstable parameters to skip based on instance equality (`===`)
|
|
- **Stability config** — use `stability_config.conf` to mark external classes as stable: `com.example.network.dto.*`, `kotlinx.datetime.Instant`
|
|
- **Compose Compiler Metrics** — audit `restartable`/`skippable` characteristics regularly
|
|
|
|
## Baseline Profiles (Android)
|
|
|
|
Pre-compile hot code paths via Jetpack Macrobenchmark to reduce startup time and jank:
|
|
|
|
```kotlin
|
|
@RunWith(AndroidBenchmarkRunner::class)
|
|
class StartupBenchmark {
|
|
@get:Rule val benchmarkRule = MacrobenchmarkRule()
|
|
|
|
@Test
|
|
fun startup() = benchmarkRule.measureRepeated(
|
|
packageName = "com.example.app",
|
|
metrics = listOf(StartupTimingMetric()),
|
|
iterations = 10,
|
|
setupBlock = { pressHome(); startActivityAndWait() }
|
|
) { /* interact with app */ }
|
|
}
|
|
```
|
|
|
|
Target <16.67ms per frame for 60fps. Use `FrameTimingMetric()` for scroll/interaction benchmarks.
|
|
|
|
### R8/ProGuard Rules for Compose (Android only)
|
|
|
|
```proguard
|
|
-keep @androidx.compose.runtime.Stable class **
|
|
-keep @androidx.compose.runtime.Immutable class **
|
|
```
|