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되거나 건너뛰는 빈도를 확인할 수 있다. tool

성능 개선 권장사항 종합

remember {...}로 값을 저장해서 Recomposition 시점에 재사용하기

Composable의 상태에 쓰이는 메모리를 관리하기

key Composable을 사용해서 불필요한 전체 항목 Recomposition을 막기

Smart Recomposition에 도움이 되는 정보 추가하기

remember { derivedStateOf {...} }로 무거운 작업의 결과를 저장해서 Recomposition 시점에 재사용하기

무거운 Blocking 작업의 결과를 remember된 상태로 변환

상태 읽기를 최대한 미뤄서 프레임3단계를 최적화 하기

프레임 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++
}