Compose 기초 - Compose 성능개선
Compose 성능개선 (원문)
💡개념이 헷갈리거나 제가 잘 모르거나 많은 사람들이 잘 모를 것 같은 것 위주로 정리
앱 설정으로 성능 개선
release 모드로 빌드하여 성능 비용이 발생하는 디버깅 코드를 삭제한다.
R8 컴파일러를 사용하여 앱에서 불필요한 코드를 삭제한다.
Baseline Profiles를 적용하여 앱 실행 시간과 jank를 감소시킨다.
*Baseline Profiles : Profile에 담긴 메소드 목록은 ART에 의해 AOT 컴파일 된다.
프레임 3단계에서 성능 개선
Composition 단계에서 성능 개선(≠Composition 단계 건너뛰기)
Recomposition에 의해 @Composable 함수가 빈번히 호출되거나 호출 제외 될 수 있기 때문에 복잡한 계산은 @Composable
함수 외부에서 실행되도록 한다. ex) ViewModel
자주 변경되는 상태는 값 대신에 값에 접근할 수 있는 람다{...}
를 Composable에 전달한다.
// 최적화되지 않은 코드
@Composable
fun Screen() {
val scroll = rememberScrollState(0)
Box(
modifier = Modifier
.scrollable(scroll, Orientation.Vertical)
.fillMaxSize(),
) {
Log.e("BSSCCO", "in Box")
// 스크롤 오프셋 scroll.value가 변경될 때마다 Screen, Title이 Recomposition 된다.
TextList(scroll.value)
}
}
@Composable
private fun TextList(scroll: Int) {
Log.e("BSSCCO", "in TextList")
val offset = with(LocalDensity.current) { scroll.toDp() }
Column(
modifier = Modifier
.offset(y = offset)
) {
val modifier = Modifier.height(200.dp)
Text(modifier = modifier, text = "a")
Text(modifier = modifier, text = "b")
Text(modifier = modifier, text = "c")
Text(modifier = modifier, text = "d")
Text(modifier = modifier, text = "e")
Text(modifier = modifier, text = "f")
Text(modifier = modifier, text = "g")
}
}
// 최적화 된 코드
@Composable
fun Screen() {
val scroll = rememberScrollState(0)
Box(
modifier = Modifier
.scrollable(scroll, Orientation.Vertical)
.fillMaxSize(),
) {
Log.e("BSSCCO", "in Box")
// scrollProvider가 Title Composable 안에서 호출되므로 스크롤 오프셋 scroll.value가 변경될 때마다 Title만 Recomposition 된다.
TextList(scrollProvider = { scroll.value })
}
}
@Composable
private fun TextList(scrollProvider: () -> Int) {
Log.e("BSSCCO", "in TextList")
val offset = with(LocalDensity.current) { scrollProvider().toDp() }
Column(
modifier = Modifier
.offset(y = offset)
) {
val modifier = Modifier.height(200.dp)
Text(modifier = modifier, text = "a")
Text(modifier = modifier, text = "b")
Text(modifier = modifier, text = "c")
Text(modifier = modifier, text = "d")
Text(modifier = modifier, text = "e")
Text(modifier = modifier, text = "f")
Text(modifier = modifier, text = "g")
}
}
상위 단계를 건너뛰어서 성능 개선
자주 변경되는 상태를 전달할 때 람다{...}
기반 Modifier를 선택한다.
// 한 번 더 최적화 된 코드
@Composable
fun Screen() {
val scroll = rememberScrollState(0)
Box(
modifier = Modifier
.scrollable(scroll, Orientation.Vertical)
.fillMaxSize(),
) {
Log.e("BSSCCO", "in Box")
// scrollProvider가 Title Composable의 Layout단계에서 호출되므로 스크롤 오프셋 scroll.value가 변경될 때마다 Title의 Layout 단계만 재실행된다.
TextList(scrollProvider = { scroll.value })
}
}
@Composable
private fun TextList(scrollProvider: () -> Int) {
Log.e("BSSCCO", "in TextList")
Column(
modifier = Modifier
// Layout 단계에서 호출되는 offset()에 람다{...}를 전달해 상태읽기를 지연시킨다.
.offset { IntOffset(x = 0, y = scrollProvider()) }
) {
val modifier = Modifier.height(200.dp)
Text(modifier = modifier, text = "a")
Text(modifier = modifier, text = "b")
Text(modifier = modifier, text = "c")
Text(modifier = modifier, text = "d")
Text(modifier = modifier, text = "e")
Text(modifier = modifier, text = "f")
Text(modifier = modifier, text = "g")
}
}
도구를 사용해 성능 문제 발견
Layout Inspector를 사용해 Composable이 Recomposition되거나 건너뛰는 빈도를 확인할 수 있다.
성능 개선 권장사항 종합
remember {...}
로 값을 저장해서 Recomposition 시점에 재사용하기
key
Composable을 사용해서 불필요한 전체 항목 Recomposition을 막기
Smart Recomposition에 도움이 되는 정보 추가하기
remember { derivedStateOf {...} }
로 무거운 작업의 결과를 저장해서 Recomposition 시점에 재사용하기
무거운 Blocking 작업의 결과를 remember된 상태로 변환
상태 읽기를 최대한 미뤄서 프레임3단계를 최적화 하기
Modifier.drawBehind()
를 사용해서 프레임 3단계를 최소화 하기
// 애니메이션 실행 시 프레임마다 Composition단계부터 재실행된다.
val color by animateColorBetween()
Log.e("BSSCCO", "RecompositionByAnim")
Box(
modifier = Modifier
.fillMaxSize()
.background(color),
)
@Composable
private fun animateBetween(): State<Color> {
val color = remember {
Animatable(Color.White)
}
LaunchedEffect(true) {
color.animateTo(Color.Green, tween(durationMillis = 3000))
}
return color.asState()
}
val color by animateBetween()
Log.e("BSSCCO", "RecompositionByAnim?")
Box(
modifier = Modifier
.fillMaxSize()
// 애니메이션 실행 시 프레임마다 Layout단계부터 재실행된다.
.drawBehind {
drawRect(color)
}
)
@Composable
private fun animateBetween(): State<Color> {
val color = remember {
Animatable(Color.White)
}
LaunchedEffect(true) {
color.animateTo(Color.Green, tween(durationMillis = 3000))
}
return color.asState()
}
Composition에서 상태를 쓰기를 하지 않음으로써 무한루프를 예방하기
// 나쁜 예
@Composable
fun BadComposable() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }, Modifier.wrapContentSize()) {
Text("Recompose")
}
Text("$count")
count++
}