[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
---
|
||||
id: android-compose-performance
|
||||
title: Compose Performance — recomposition / stability
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [android, compose, performance, vibe-coding]
|
||||
tech_stack: { language: "Kotlin", applicable_to: ["Android"] }
|
||||
applied_in: []
|
||||
aliases: [Compose performance, recomposition, stability, immutable, baseline profile, strong skipping mode]
|
||||
---
|
||||
|
||||
# Compose Performance
|
||||
|
||||
> Recomposition 폭발 = jank. **Stable / immutable, derivedStateOf, baseline profile, strong skipping mode**.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Recomposition = render again.
|
||||
- Stable = type 가 변경 시 비교 가능 → skip 가능.
|
||||
- Strong skipping mode (Kotlin 2.0+) = 자동 skip.
|
||||
- Baseline profile = startup 빠름.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Recomposition trace
|
||||
```kotlin
|
||||
@Composable
|
||||
fun MyComponent() {
|
||||
SideEffect {
|
||||
Log.d('Compose', 'recomposed')
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
→ Frequency 측정.
|
||||
|
||||
### Stability annotation
|
||||
```kotlin
|
||||
@Stable
|
||||
data class User(
|
||||
val id: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class Config(...)
|
||||
```
|
||||
|
||||
→ Compose 가 skip 가능.
|
||||
|
||||
### Unstable의 함정
|
||||
```kotlin
|
||||
// ❌ List 가 unstable
|
||||
@Composable
|
||||
fun ItemList(items: List<Item>) {
|
||||
// 매 recomposition.
|
||||
}
|
||||
|
||||
// ✅ ImmutableList (kotlinx.collections.immutable)
|
||||
@Composable
|
||||
fun ItemList(items: ImmutableList<Item>) {
|
||||
// Skip 가능.
|
||||
}
|
||||
```
|
||||
|
||||
→ kotlinx.collections.immutable.
|
||||
|
||||
### Lambda stability
|
||||
```kotlin
|
||||
// ❌ 매 recomposition 가 새 lambda
|
||||
@Composable
|
||||
fun Screen(viewModel: ViewModel) {
|
||||
Button(onClick = { viewModel.action() }) { ... }
|
||||
}
|
||||
|
||||
// ✅ Method reference (stable)
|
||||
@Composable
|
||||
fun Screen(viewModel: ViewModel) {
|
||||
Button(onClick = viewModel::action) { ... }
|
||||
}
|
||||
|
||||
// ✅ remember
|
||||
@Composable
|
||||
fun Screen() {
|
||||
val onClick = remember { { /* ... */ } }
|
||||
Button(onClick = onClick) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Strong Skipping Mode (Kotlin 2.0+)
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add('-P plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ 자동 skip (lambda 도). Magic 식.
|
||||
|
||||
### derivedStateOf
|
||||
```kotlin
|
||||
@Composable
|
||||
fun Screen(items: List<Item>) {
|
||||
val hasMore by remember(items) {
|
||||
derivedStateOf { items.size > 100 }
|
||||
}
|
||||
|
||||
// hasMore 가 변경 시 만 update.
|
||||
}
|
||||
```
|
||||
|
||||
→ Computed value memoize.
|
||||
|
||||
### key (recomposition 정밀)
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ItemList(items: List<Item>) {
|
||||
LazyColumn {
|
||||
items(items, key = { it.id }) { item ->
|
||||
ItemRow(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ Item 의 identity. Reorder 시 잘못 recompose 안.
|
||||
|
||||
### LazyColumn / LazyRow
|
||||
```kotlin
|
||||
LazyColumn {
|
||||
items(1000) { index ->
|
||||
Card { Text('item $index') }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ Visible 만 render. RecyclerView 의 modern.
|
||||
|
||||
### Layout inspector
|
||||
```
|
||||
Android Studio:
|
||||
- Layout Inspector.
|
||||
- Compose tree.
|
||||
- Recomposition count per node.
|
||||
```
|
||||
|
||||
→ "이 node 가 가짜 recompose?".
|
||||
|
||||
### Tracing
|
||||
```kotlin
|
||||
// In code
|
||||
import androidx.compose.runtime.tooling.*
|
||||
|
||||
Trace.beginSection('MyComponent')
|
||||
// ...
|
||||
Trace.endSection()
|
||||
```
|
||||
|
||||
→ Systrace 가 visible.
|
||||
|
||||
### Baseline profile
|
||||
```kotlin
|
||||
// macrobenchmark module
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BaselineProfileGenerator {
|
||||
@get:Rule val rule = BaselineProfileRule()
|
||||
|
||||
@Test
|
||||
fun generate() = rule.collect(packageName = 'com.example.app') {
|
||||
startActivityAndWait()
|
||||
// critical user flow
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ AOT compile of hot code. Cold start 30-50% 빠름.
|
||||
|
||||
### Stability inspect
|
||||
```bash
|
||||
./gradlew assembleRelease -PenableComposeCompilerReports=true
|
||||
|
||||
# Generated:
|
||||
# build/compose_metrics/-classes.txt
|
||||
# - Stable / unstable 매 class.
|
||||
```
|
||||
|
||||
→ "왜 unstable?" 검증.
|
||||
|
||||
### List 의 함정
|
||||
```kotlin
|
||||
// ❌
|
||||
data class State(val items: List<String>)
|
||||
// → List 가 unstable.
|
||||
|
||||
// ✅
|
||||
data class State(val items: ImmutableList<String>)
|
||||
```
|
||||
|
||||
### Function 의 함정
|
||||
```kotlin
|
||||
// ❌ Function type 가 unstable 가 default
|
||||
data class State(val onClick: () -> Unit)
|
||||
|
||||
// ✅
|
||||
@Stable data class State(val onClick: () -> Unit)
|
||||
```
|
||||
|
||||
→ Strong skipping mode 가 자동 fix.
|
||||
|
||||
### remember vs derivedStateOf
|
||||
```kotlin
|
||||
val sorted = remember(items) { items.sortedBy { it.name } }
|
||||
// → items 가 변경 시 recalculate.
|
||||
|
||||
val hasMore by remember { derivedStateOf { items.size > 100 } }
|
||||
// → items 가 변경 + hasMore 가 변경 시 만 update.
|
||||
```
|
||||
|
||||
→ derivedStateOf 가 fine-grained.
|
||||
|
||||
### Immutable collection
|
||||
```kotlin
|
||||
import kotlinx.collections.immutable.*
|
||||
|
||||
val list: ImmutableList<Item> = persistentListOf(...)
|
||||
val map: ImmutableMap<String, Int> = persistentMapOf(...)
|
||||
```
|
||||
|
||||
→ Compose 가 stable 인식.
|
||||
|
||||
### Stop animation 시
|
||||
```kotlin
|
||||
val transition = rememberInfiniteTransition()
|
||||
val rotation by transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(...)
|
||||
)
|
||||
|
||||
// Visible 안 = 자동 stop.
|
||||
```
|
||||
|
||||
→ Battery 친화.
|
||||
|
||||
### Modifier order
|
||||
```kotlin
|
||||
// Order 가 important
|
||||
Box(modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.background(Color.Red)
|
||||
.size(100.dp)
|
||||
)
|
||||
```
|
||||
|
||||
→ Modifier 가 chain. 매 modifier 가 cost.
|
||||
|
||||
### Profile
|
||||
```kotlin
|
||||
// Layout Inspector → Recomposition Count.
|
||||
// 큰 number = bottleneck.
|
||||
|
||||
// Profile mode (release):
|
||||
./gradlew assembleRelease
|
||||
adb install ...
|
||||
adb shell setprop debug.layout true
|
||||
```
|
||||
|
||||
### CompositionLocal 의 함정
|
||||
```kotlin
|
||||
val LocalUser = compositionLocalOf<User> { error('not provided') }
|
||||
|
||||
// 매 user 변경 = 매 consumer recompose.
|
||||
```
|
||||
|
||||
→ 자주 변경 = unstable.
|
||||
|
||||
### Jetpack Compose Compiler Metrics
|
||||
```bash
|
||||
# build/compose_metrics/
|
||||
- *-classes.txt (stability)
|
||||
- *-composables.txt (skippable, restartable)
|
||||
```
|
||||
|
||||
→ Skippable: input 같음 = skip OK.
|
||||
Restartable: trigger restart of recomposition.
|
||||
|
||||
### Performance tier
|
||||
```
|
||||
1. Layout Inspector (initial check).
|
||||
2. Compose Compiler Reports (stability).
|
||||
3. Tracing (Trace.beginSection).
|
||||
4. Baseline profile (startup).
|
||||
5. Macrobenchmark (real device).
|
||||
```
|
||||
|
||||
### Real-world tips
|
||||
```
|
||||
- 큰 list = LazyColumn + key.
|
||||
- ImmutableList default.
|
||||
- Strong skipping mode (Kotlin 2.0+).
|
||||
- Method reference > lambda.
|
||||
- @Stable / @Immutable annotation.
|
||||
- Profile 매 release.
|
||||
```
|
||||
|
||||
### vs RecyclerView (옛)
|
||||
```
|
||||
RecyclerView: imperative, 빠름.
|
||||
LazyColumn: declarative, 비슷 빠름.
|
||||
|
||||
→ 현재 = LazyColumn.
|
||||
```
|
||||
|
||||
### Common 함정
|
||||
```
|
||||
- List 가 unstable.
|
||||
- Lambda 가 매번 새.
|
||||
- ViewModel state 가 큰 object.
|
||||
- 매 recomposition 가 expensive work.
|
||||
- LazyColumn 가 key 없음.
|
||||
- Modifier 가 매번 새.
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 작업 | 추천 |
|
||||
|---|---|
|
||||
| Custom data class | @Stable / @Immutable |
|
||||
| List | ImmutableList |
|
||||
| Lambda | Method reference / remember |
|
||||
| Compute | derivedStateOf |
|
||||
| Big list | LazyColumn + key |
|
||||
| Cold start | Baseline profile |
|
||||
| Profile | Layout Inspector + tracing |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **List 가 unstable**: skip 안 됨.
|
||||
- **Lambda 가 매번**: recompose 폭발.
|
||||
- **No key in LazyColumn**: identity 깨짐.
|
||||
- **CompositionLocal 자주 변경**: 폭발.
|
||||
- **Big object state**: 매 변경 = 큰 recompose.
|
||||
- **Profile 안 함**: blind.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Stable / immutable 가 핵심.
|
||||
- Strong skipping mode (Kotlin 2.0+).
|
||||
- Baseline profile 가 cold start.
|
||||
- Layout Inspector 가 visible.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Android_Compose_Recomposition_Pitfalls]]
|
||||
- [[Android_Compose_State_Hoisting]]
|
||||
- [[Android_Baseline_Profile]]
|
||||
Reference in New Issue
Block a user