Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Benchmark

 Benchmark

Compose 성능 최적화 요소는 무엇이 있을까요?

Stable 한 함수 설계부터 시작해, Frame Rendering 3단계를 염두한 Composition 건너뛰기, 이미지 로딩 방식의 이해 등, 여러 가지가 있습니다!

제가 만났던 문제들을 Benchmark, Baseline Profile, Perfetto, Layout Inspector를 활용하며 진단해 나가고, 해결한 저의 여정을 공유합니다!

Avatar for 송상윤

송상윤

June 18, 2025
Tweet

Other Decks in Programming

Transcript

  1. 송상윤 | Android Developer 👉 (주)모비두 / (주)펜타시큐리티 👉 블로그

    : velog.io@squart300kg 👉 깃허브 : github.com/squart300kg ㄴ
  2. 이미지 영역 블랙 상자 지우고 사용하세요 1단계 : javac 컴파일

    👉 ʻjavac’컴파일러는 .java 확장자의 소스코드를 빌드하여 .class확장자의 바이트 코드를 생성한다.
  3. 👉 ʻKotlinc’컴파일러는 .kotlin 확장자의 소스코드를 빌드하여 .class 확장자의 바이트 코드를

    우선적으로 생성한다. 👉 Kotlin은 확장 함수 등으로 Java 클래스를 상속 없이 확장하는 형태로, 전용 API를 제공한다. 👉 ʻKotlinc’컴파일러는 .class 바이트 코드와 코틀린 전용 API를 묶어 .jar확장자의 바이트 코드를 생성한다. 1단계 : kotlinc 컴파일
  4. 👉 Android Studio에 있는 ʻR8’ 컴파일러는 .class or .jar 확장자의

    바이트 코드를 컴파일하여 .dex 바이트 코드를 생성한다. 👉 .dex 바이트 코드와 각종 리소스 파일들이 합쳐져 .apk파일이 생성된다. 2단계 : R8 컴파일
  5. 👉 .apk파일을 .zip 확장자로 변경 후, 열어보자. 👉 확인해보면 .dex파일들을

    확인할 수 있다. 이미지 영역 블랙 상자 지우고 사용하세요 3단계 : R8 컴파일
  6. 이미지 영역 블랙 상자 지우고 사용하세요 3단계 : AOT /

    JIT 컴파일 👉 AOT : 앱을 스토어에서 다운로드할 때, .dex를 AndroidOS가 이해 가능한 기계어로 사전 컴파일 👉 JIT : 앱 실행 or 중간에 기능을 동작시킬 때, .dex를 AndroidOS가 이해 가능안 기계어로 그때그때 컴파일 .oat odex .vdex 등…
  7. 👉 AOT : 앱을 스토어에서 다운로드할 때, .dex를 AndroidOS가 이해

    가능한 기계어로 사전 컴파일 👉 JIT : 앱 실행 or 중간에 기능을 동작시킬 때, .dex를 AndroidOS가 이해 가능한 기계어로 그때그때 컴파일 3단계 : AOT / JIT 컴파일 .oat odex .vdex 등…
  8. @RunWith(AndroidJUnit4::class) class ScrollBenchmarks { @get:Rule val benchmarkRule = MacrobenchmarkRule() @Test

    fun scroll() { benchmarkRule.measureRepeated( packageName = "com.example.macrobenchmark_codelab", compilationMode = CompilationMode.None(), iterations = 5, metrics = listOf(FrameTimingMetrics()), startupMode = StartupMode.COLD, setupBlock = { } ) { } }
  9. CompilationMode? 👉 CompilationMode().None 👉 CompilationMode().Partial 👉 CompilationMode().Full What? 앱 실행

    전, 코드가 어느 정도까지 컴파일되어야 하는지를 나타내는 설정으로, 성능 벤치마크 실행 환경을 제어하는데 사용된다.
  10. 👉 앱 설치 시, 어떠한 사전 컴파일도 진행하지 않겠다는 선언으로

    100% JIT 컴파일 사용을 의미한다. 👉 따라서 None 파라미터를 통해 벤치마크 or 앱 실행 시 앱의 런타임 성능이 안좋게 나온다. .None() 2 Benchmark 성능 측정 옵션 JIT : 28.4ms
  11. 👉 APK의 모든 .dex에 AOT컴파일을 적용할 때 사용한다. 👉 앱

    사전 컴파일로 인해 런타임 성능은 가장 좋으나, 설치 속도와 앱 용량 이슈가 존재한다. 👉 현재 ART는 JIT + AOT 하이브리드 환경으로, 실 사용자 환경과 유사하지 않으며, 성능 개선 확인 용도로 확인한다. .Full() 2 Benchmark 성능 측정 옵션 AOT : 10.3ms
  12. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 현재 버전의 ART는 AOT와 JIT이 혼합된 하이브리드 방식으로 동작하며, 어떤 코드가 AOT 또는 JIT 방식으로 컴파일될지는 내부적으로 결정되기 때문에 개발자가 예측하기 어렵다. 따라서 렌더링 속도가 중요한 앱의 특정 지점에서는 Baseline Profile을 적용 AOT 컴파일의 보장 및 렌더링 성능을 개선할 수 있다.
  13. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 즉, 실 사용자 환경과 가장 유사한 벤치마크 방식은 .Partial()이다.
  14. 👉 실 사용자 환경에서 ART는 앱에서 자주 사용되는 .dex를 localProfile에

    저장 후, AOT컴파일을 점진적으로 진행한다. 👉 이때, 개발자는 BaselineProfile을 사용해 AOT컴파일이 진행 될 지점을 사전 지정 가능하다. 👉 하지만 AOT의 부분 지정인 만큼 앱의 다른 부분은 JIT컴파일 방식이 적용됨을 유의한다. .Partial() 2 Benchmark 성능 측정 옵션 AOT : 11.6ms
  15. @RunWith(AndroidJUnit4::class) class ScrollBenchmarks { @get:Rule val benchmarkRule = MacrobenchmarkRule() @Test

    fun scroll() { benchmarkRule.measureRepeated( packageName = "com.example.macrobenchmark_codelab", compilationMode = CompilationMode.None(), iterations = 5, metrics = listOf(FrameTimingMetrics()), startupMode = StartupMode.COLD, setupBlock = { } ) { } }
  16. 👉 FrameTimingMetrics 안드로이드 앱의 UI 렌더링은 60FPS 즉, 1초에 60

    Frame(1 frame = 16.67ms)을 그려야 UI의 버벅거림이 없다. 만약, 1 Frame을 그리는데 16.67ms보다 더 걸린다면 Choreographer가 해당 Frame을 삭제하여 버벅거림이 발생하게 된다. [측정 지표] 👉 frameDurationCpuMs 👉 frameOverrunMs Choreographer란? 디스플레이가 화면을 갱신하는 주기인 vsync 타이밍에 맞춰, UI 렌더링 시점을 조율하는 안드로이드의 프레임 스케줄러 2 Benchmark 성능 측정 옵션
  17. CPU가 1 Frame을 그리는데 걸린 연산 시간으로, 이 수치가 높다는

    것은 UI Frame을 그리는 초기 단계부터 문제가 생겼다는 것을 의미한다. 👉 frameDurationCpuMs 2 Benchmark 성능 측정 옵션
  18. Graphic작업(GPU or Composable함수 렌더링) 의 연산 시간을 의미하며, 1 Frame(16.67ms)을

    그리는데 초과된 시간을 의미한다. 음수일 경우 16.67ms보다 빠른 렌더링을, 양수일 경우 16.67ms보다 느린 렌더링을 의미한다 👉 frameOverrunMs 2 Benchmark 성능 측정 옵션
  19. 👉 백분위 해석법 2 Benchmark 성능 측정 옵션 0% 100%

    50% 전체 프레임의 50% 까지가 16.67 - 1.2ms 이내 렌더링 완료
  20. 👉 백분위 해석법 2 Benchmark 성능 측정 옵션 0% 100%

    50% 전체 프레임의 90% 까지가 16.67 + 4.8ms 이내 렌더링 완료 90%
  21. 👉 백분위 해석법 2 Benchmark 성능 측정 옵션 0% 100%

    50% 전체 프레임의 95% 까지가 16.67 + 12.8ms 이내 렌더링 완료 90% 95%
  22. 👉 백분위 해석법 2 Benchmark 성능 측정 옵션 0% 100%

    50% 전체 프레임의 99% 까지가 16.67 + 28.4ms 이내 렌더링 완료 90%95%99%
  23. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 [시사점1] frameDurationCpuMs의 P50가 16.67이상으로 나온다면 렌더링 이슈 발생 확률이 높다. 전체 프레임의 50%에서 CPU 연산 시간이 16.67ms(60fps 기준 1프레임 시간)를 초과했다는 것은, 프레임 렌더링 초기 단계에서부터 문제가 발생했음을 의미하기 때문이다. Why?
  24. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 [시사점2] frameOverrunMs의 P99가 음수로 나온다면 전체적인 렌더링 성능은 우수하다고 볼 수 있다. 대부분의 프레임 마감 시간(16.67ms)보다 여유 있게 처리되었음을 의미하기 때문이다. Why?
  25. 3 UI 성능 개선 여정 Rester Image의 렌더링 이슈 개선하기

    1 Image로딩 최적화 2 Compose Frame Rendering 최적화 Recomposition 이슈 개선하기
  26. Composable함수 파라미터는 모두 stable하며, Recomposition이슈는 없다! LazyColumn { items( items

    = uiState.stockAppList, key = { it.code } ) { stockApp -> StockAppItem( stockApp = stockApp, onClickedStockApp = { ... } ) } } @Composable private fun StockAppItem( stockApp: StockEnum, onClickedStockApp: () -> Unit = {} ) { ... } 👉 문제 진단 시도 1 : Recomposition? 3 UI 성능 개선 여정
  27. 정상이라면 1 frame의 rendering시간은 16ms이나 현재는 908.6 + 16.67ms이다. 👉

    문제 진단 시도 2 : Frame Rendering? 3 UI 성능 개선 여정
  28. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 Image( modifier = Modifier .size(40.dp), painter = painterResource(), contentDescription = null ) 👉 병목 구간 찾기 3 UI 성능 개선 여정 Box( modifier = Modifier .size(40.dp) .background(Color.Magenta) )
  29. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 👉 병목 구간 찾기 3 UI 성능 개선 여정
  30. 1 병목 구간은 Image() composable함수이다. 2 이미지 로딩 방식은 LazyColumn()의

    스크롤 성능에 영향을 준다. 3 UI 성능 개선 여정
  31. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 Image( modifier = Modifier .size(...) painter = painterResource(...), contentDescription = null ) 👉 최적화 1단계 : ImageBitmap을 coil로 로딩 Image( modifier = Modifier .size(...) painter = rememberAsyncImagePainter(...), contentDescription = null ) Before After 3 UI 성능 개선 여정
  32. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 👉 최적화 1단계 : ImageBitmap을 coil로 로딩 Before After 3 UI 성능 개선 여정
  33. 👉 Perfetto란? 프레임 렌더링 정밀 추적 도구 렌더링 지연, 프레임

    드롭, CPU/GPU 병목 등의 문제를 타임라인에 기반하여 정밀하게 추적 가능 분석하고자 하는 UI의 요소를 SQLite에 기반하여 커스텀한 성능 분석 가능 3 UI 성능 개선 여정
  34. 👉 이미지 로딩 방식 [Rester이미지] : png, jpg 등, pixel기반의

    이미지 Main Thread를 점유하며 로딩이 이뤄져 frame rendering성능에 영향을 미침. 따라서 고화질의 Rester이미지 로딩을 위해선 coil등 비동이 이미지 라이브러리 사용이 권장됨. [Vector이미지] : .xml, .kt 등, pixel과 무관하게 점,선,면 등 수학 공식으로 만들어진 이미지 composable환경의 Vector이미지는 Vector Image Tree를 구축하여 그려지는데, 이를 통해 Main Thread의 리소스를 더 아낄 수 있음. 3 UI 성능 개선 여정
  35. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 👉 최적화 2단계 : VectorImage(.xml)을 coil로 로딩 출처 : 안드로이드 공식 홈페이지 3 UI 성능 개선 여정
  36. 세로로 긴 이미지 쓰고 싶으면 사용해주세요 설명은 필요시 여기에 적어주세요,

    타이틀이 길어지면 위치를 아래로 내리셔도 됩니다 👉 최적화 2단계 : VectorImage(.xml)을 coil로 로딩 1 ResterImage들을 VectorImage(.xml)로 변경 2 Benchmark로 성능을 측정 3 UI 성능 개선 여정
  37. 3 UI 성능 개선 여정 그 이유는 Compose환경에서 vector이미지를 로딩하려면

    Vector Image Tree를 그려야 함 (Composable구성을 위해 Layout Node Tree를 그리는 것과 유사) 따라서 .xml 이미지 또한 Vector Image Tree로 표현되기 위해 ImageVector로의 변환 작업이 진행됨. 하지만 이 변환 작업은 Main Thread를 Blocking한다. 따라서 .xml타입의 벡터 이미지는 .kotlin 이미지보다 더 느리다.
  38. .xml 3 UI 성능 개선 여정 👉 최적화 3단계 :

    VectorImage(.kotlin) 로딩 .kotlin 로딩 시간 300µs 로딩 시간 80µs
  39. 3 UI 성능 개선 여정 👉 최적화 4단계 : RadioButton

    커스텀하게 구현하기 RadioButton( selected = uiState.selectedStockApp == stockApp, colors = RadioButtonDefaults.colors().copy( selectedColor = colorResource(...), unselectedColor = colorResource(...) ), onClick = { ... } ) RadioButton들
  40. 3 UI 성능 개선 여정 👉 최적화 4단계 : RadioButton

    커스텀하게 구현하기 1개 아이템 렌더링 시간 : 1000µs 1개 아이템 내, RadioButton 렌더링 시간 : 260µs
  41. 3 UI 성능 개선 여정 Box( modifier = Modifier .border(

    shape = CircleShape, border = borderState ) .clickable(...) ) ) 👉 최적화 4단계 : RadioButton 커스텀하게 구현하기 RadioButton( selected = ..., colors = RadioButtonDefaults.colors().copy( selectedColor = colorResource(...), unselectedColor = colorResource(...) ), onClick = { ... } )
  42. 3 UI 성능 개선 여정 👉 최적화 4단계 : RadioButton

    커스텀하게 구현하기 Before : 300µs After : 100µs
  43. 3 UI 성능 개선 여정 Rester Image의 렌더링 이슈 개선하기

    1 Image로딩 최적화 2 Compose Frame Rendering 최적화 Recomposition 이슈 개선하기 3 UI 성능 개선 여정
  44. 👉 Compose Frame Rendering 최적화 사전 지식 [composable함수의 frame rendering

    3단계] • Composition • Layout(1. Measure 2. Place) • Drawing [상태값 읽는 시점의 중요성] Layout or Drawing단계 때 상태값 읽기/변경 작업은 불필요한 Composition작업을 반복시킨다.
  45. 👉 문제 정의 3 UI 성능 최적화 1 하단 스크롤(0.0f

    ~ 1.0f) 2 fontSize 변경 Text( text = tradingDetailProfileVo.stock, fontSize = 12.sp * (1 - scrollProgressState) )
  46. 👉 솔루션1. Modifier.layout { ... }을 사용하여 상태 읽기 val

    heightToPx by rememberUpdatedState( ((1 - scrollProgressState) * with (density) { 18.dp.roundToPx() }).toInt() ) Text( modifier = Modifier .layout { measurable, constraints -> val placeable = measurable.measure( Constraints.fixedHeight(height = heightToPx) ) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } text = tradingDetailProfileVo.stock, fontSize = 12.sp fontSize 파라미터로 상태값을 읽는 게 아닌, (Composition단계 때 읽지 않음) Layout단계 때 상태값을 읽은 후, 이를 height에 적용
  47. 👉 솔루션1. Modifier.layout { ... }을 사용하여 상태 읽기 3

    UI 성능 최적화 1 하단 스크롤(0.0f ~ 1.0f) 2 fontSize 변경 Text( modifier = Modifier .layout { … } text = tradingDetailProfileVo.stock, fontSize = 12.sp )
  48. 3 UI 성능 개선 여정 Modifier.layout { ... }을 확장함수로

    추출하고픈 욕구... 하지만 결과는? fun Modifier.heightWithNoComposition(height: Int): Modifier = this.layout { measurable, _ -> val constraints = Constraints.fixedHeight( height = height ) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } }
  49. 👉 솔루션1. Modifier.layout { ... }을 사용하여 상태 읽기 3

    UI 성능 최적화 1 하단 스크롤(0.0f ~ 1.0f) 2 fontSize 변경 Text( modifier = Modifier .heightWithNoComposition( … ) text = tradingDetailProfileVo.stock, fontSize = 12.sp )
  50. 👉 솔루션1. Modifier.layout { ... }을 사용하여 상태 읽기 [이유1]

    scrollState상태 변수가 최 상위 Composable 함수에 주입되어, Compose Runtime이 Recomposition을 예약. [이유2] Compose Runtime이 하위 composable함수들의 skippable여부를 검사. 이때, scrollState는 Modifier의 파라미터에 주입되므로, Recomposition이 예약 됨.
  51. 👉 솔루션2. Lambda를 통해 Layout단계 때, 상태 읽기 // 최

    상위 Composable함수 @Composable fun TradingDetailTopBar( // scrollProgressState: Float, scrollProgressStateProvider: () -> Float, ) { ... }
  52. 3 UI 성능 개선 여정 그 후, 하위 Composable함수 또한

    Layout단계 때 상태값을 읽도록 수정
  53. 👉 솔루션2. Lambda를 통해 Layout단계 때, 상태 읽기 // Lambda를

    사용하여 Layout단계 때 상태값 전달 val heightToPx by rememberUpdatedState( { ((1 - scrollProgressState) * with (density) { 18.dp.roundToPx() }).toInt() } ) Text( modifier = Modifier .heightWithNoComposition(heightToPx), text = tradingDetailProfileVo.stock, fontSize = 12.sp ) ) // 하위 Composable에 적용 될 Modifier.layout 확장 함수 fun Modifier.heightWithNoComposition( // height: Float, heightProvider: () -> Float ): Modifier = this.layout { … }
  54. 👉 솔루션2. Lambda를 통해 Layout단계 때, 상태 읽기 3 UI

    성능 최적화 1 하단 스크롤(0.0f ~ 1.0f) 2 fontSize 변경 Text( modifier = Modifier .heightWithNoComposition { … } text = tradingDetailProfileVo.stock, fontSize = 12.sp )
  55. 마치며... 바쁜 시간 내어 제 발표를 들어주신 모든 분들께 감사드립니다.

    오늘 다룬 성능 최적화는 이미지나 리컴포지션에만 해당되지 않습니다. 왜냐하면 앱 성능은 아키텍처, 하드웨어 등 다양한 요소에 의해 좌우되며, 개선할 수 있는 지점도 훨씬 많을거라 보기 때문입니다. 크로스플랫폼도 튜닝은 가능합니다. 하지만 네이티브 개발자는 OS와 더 가까운 만큼 플랫폼을 직접적으로 이해할 수 있고, 문제를 파악하여 개선할 수 있다는 점에서 경쟁력이 있다 믿습니다. 혹시 다른 방식의 최적화 경험이 있으시다면, 제 블로그에 댓글로 공유해 주세요. 함께 나누면 더 좋은 인사이트가 될 수 있을 것 같습니다. 감사합니다!