자주 바뀌는 입력에서 드물게 바뀌는 결과를 뽑아 리컴포지션을 줄인다.
val listState = rememberLazyListState()
// ❌ 스크롤 1픽셀마다 리컴포지션
val showButton = listState.firstVisibleItemIndex > 0
// ✅ Boolean 이 바뀔 때만
val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
| 입력 (Int) | 0 → 1 → 2 → 3 → 4 → 5 ... | 초당 수십 번 바뀐다 |
|---|---|---|
| 출력 (Bool) | false → true | 한 번만 바뀐다 |
- 출력이 바뀔 때만 구독자를 무효화한다
언제 쓰지 않나
// ❌ 입력과 출력이 같은 빈도로 바뀐다 — 오버헤드만 늘어난다
val fullName by remember { derivedStateOf { "$first $last" } }
// ✅ 그냥 계산하거나 remember(key) 를 쓴다
val fullName = remember(first, last) { "$first $last" }
판별
- "입력이 100번 바뀔 때 출력이 몇 번 바뀌나?"
- 비슷하다 → derivedStateOf 는 손해다
- 훨씬 적다 → 이득이다
읽기 지연 — 람다로 넘긴다
// ❌ 이 컴포저블이 offset 을 읽는다 → 스크롤마다 전체가 다시 그려진다
Box(Modifier.offset(y = scrollOffset.dp))
// ✅ 람다 버전은 그리기 단계에서만 읽는다 → 컴포지션을 건너뛴다
Box(Modifier.offset { IntOffset(0, scrollOffset) })
Compose 는 세 단계로 돈다
- 컴포지션(무엇을) → 레이아웃(어디에) → 드로잉(어떻게)
상태를 어느 단계에서 읽느냐가 비용을 가른다
- 컴포지션에서 읽으면 세 단계 전부 다시
- 드로잉에서 읽으면 그리기만 다시
- graphicsLayer { alpha = a } · drawBehind { } 도 같은 원리다
상태 읽기 범위를 좁힌다
// ❌ 부모가 상태를 읽어 전체가 무효화된다
@Composable fun Screen(vm: VM) {
val count = vm.count
Header(); Body(); Footer(count)
}
// ✅ 필요한 곳만 읽는다
@Composable fun Screen(vm: VM) {
Header(); Body(); Footer(count = { vm.count })
}
면접 함정
- ❌
derivedStateOf를remember없이 쓴다 → 리컴포지션마다 새 파생 상태가 만들어져 아무 이득이 없다. - ❌ "모든 계산을 derivedStateOf로 감싸면 빨라진다" → 구독 관리 비용이 붙는다. 빈도 차이가 있을 때만 쓴다.
스크롤 성능 실측 예
1,000개 목록에서 '맨 위로' 버튼 표시 조건을 계산한다
- 직접 읽기 — 스크롤 1초에 리컴포지션 약 90회 (프레임마다)
- derivedStateOf — 같은 구간에서 2회 (false→true, true→false)
버튼 하나가 아니라 그 버튼을 감싼 스코프 전체가 다시 그려지므로 체감 차이가 크다
세 단계별 읽기 API
// 컴포지션에서 읽는다 — 가장 비싸다
Box(Modifier.padding(top = offset.dp))
// 레이아웃에서 읽는다
Box(Modifier.offset { IntOffset(0, offset) })
// 드로잉에서 읽는다
Box(Modifier.graphicsLayer { translationY = offset.toFloat(); alpha = a })
Box(Modifier.drawBehind { drawRect(color) })
애니메이션·제스처처럼 값이 프레임마다 바뀌면 반드시 람다 버전을 쓴다 — 이것만으로 잰크가 사라지는 경우가 많다
콜백을 안정적으로 넘긴다
// ❌ 매 리컴포지션마다 새 람다 → 하위가 skip 되지 않는다 (Strong Skipping 이전)
UserList(users, onClick = { id -> viewModel.select(id) })
// ✅ 메서드 참조는 안정적이다
UserList(users, onClick = viewModel::select)