7.3 KiB
7.3 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| android-compose-performance | Compose Performance — recomposition / stability | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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
@Composable
fun MyComponent() {
SideEffect {
Log.d('Compose', 'recomposed')
}
// ...
}
→ Frequency 측정.
Stability annotation
@Stable
data class User(
val id: String,
val name: String,
)
@Immutable
data class Config(...)
→ Compose 가 skip 가능.
Unstable의 함정
// ❌ 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
// ❌ 매 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+)
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add('-P plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true')
}
}
→ 자동 skip (lambda 도). Magic 식.
derivedStateOf
@Composable
fun Screen(items: List<Item>) {
val hasMore by remember(items) {
derivedStateOf { items.size > 100 }
}
// hasMore 가 변경 시 만 update.
}
→ Computed value memoize.
key (recomposition 정밀)
@Composable
fun ItemList(items: List<Item>) {
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(item)
}
}
}
→ Item 의 identity. Reorder 시 잘못 recompose 안.
LazyColumn / LazyRow
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
// In code
import androidx.compose.runtime.tooling.*
Trace.beginSection('MyComponent')
// ...
Trace.endSection()
→ Systrace 가 visible.
Baseline profile
// 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
./gradlew assembleRelease -PenableComposeCompilerReports=true
# Generated:
# build/compose_metrics/-classes.txt
# - Stable / unstable 매 class.
→ "왜 unstable?" 검증.
List 의 함정
// ❌
data class State(val items: List<String>)
// → List 가 unstable.
// ✅
data class State(val items: ImmutableList<String>)
Function 의 함정
// ❌ Function type 가 unstable 가 default
data class State(val onClick: () -> Unit)
// ✅
@Stable data class State(val onClick: () -> Unit)
→ Strong skipping mode 가 자동 fix.
remember vs derivedStateOf
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
import kotlinx.collections.immutable.*
val list: ImmutableList<Item> = persistentListOf(...)
val map: ImmutableMap<String, Int> = persistentMapOf(...)
→ Compose 가 stable 인식.
Stop animation 시
val transition = rememberInfiniteTransition()
val rotation by transition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(...)
)
// Visible 안 = 자동 stop.
→ Battery 친화.
Modifier order
// Order 가 important
Box(modifier = Modifier
.padding(8.dp)
.background(Color.Red)
.size(100.dp)
)
→ Modifier 가 chain. 매 modifier 가 cost.
Profile
// Layout Inspector → Recomposition Count.
// 큰 number = bottleneck.
// Profile mode (release):
./gradlew assembleRelease
adb install ...
adb shell setprop debug.layout true
CompositionLocal 의 함정
val LocalUser = compositionLocalOf<User> { error('not provided') }
// 매 user 변경 = 매 consumer recompose.
→ 자주 변경 = unstable.
Jetpack Compose Compiler Metrics
# 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.