Slide 1

Slide 1 text

Compose の LazyColumn パフォーマンス改善で取り組 んだこと 2022/06/14 Engineer LT#1 Android tomoya0x00 1

Slide 2

Slide 2 text

About me tomoya0x00 Twitter, GitHub Android U-NEXT Co., Ltd. 2

Slide 3

Slide 3 text

今回のLTが(もしかしたら)役に立つかもしれない? LazyColumnって、RecyclerViewよりもパフォーマンス良いんだよね? Composeのパフォーマンス問題って、Releaseビルドにしたら解決するんでしょ? 最新技術のComposeなんだから、従来のAndroid Viewよりもパフォーマンスで悩むこと は少ないはず! 3

Slide 4

Slide 4 text

お話しすること LazyColumnを気軽に使ったらパフォーマンス問題に直面し、 なんとかNexus 7 2013という低スペック端末でもある程度使えるレベルにした話。 4

Slide 5

Slide 5 text

目次 パフォーマンスに絶望した話 パフォーマンス問題に対する考察 対策その1: Glideを使ってみる 対策その2: さらにインスタンス生成を減らす 対策その3: 描画を遅延させてスキップ可能とする これまでのような対策の前に確認しておくと良い事 他にも効果がありそうな事 まとめと感想 5

Slide 6

Slide 6 text

パフォーマンスに絶望した話 6

Slide 7

Slide 7 text

まずは動画をご覧下さい 7

Slide 8

Slide 8 text

8

Slide 9

Slide 9 text

パフォーマンスに絶望した話 Nexus 7 2013で動かしました アニメーションGIFのフレームレートは30fpsです アニメーションGIFだからカクカクしているわけではありません Releaseビルドです もちろん、minifyEnabled true LazyRow in LazyColumnで、各itemではCard内でCoilにより画像表示しているだけのシ ンプルなアプリです ちなみに、RecyclerViewだとさくさく動きます ソースコードのリンク: MainContentA 9

Slide 10

Slide 10 text

このままだとCompose化の夢が絶たれてしまう・・・ 10

Slide 11

Slide 11 text

パフォーマンス問題に対する考察 11

Slide 12

Slide 12 text

logcat GCのログが多いような気がする Background partial concurrent mark sweep GC freed 12609(781KB) AllocSpace objects, 9(180KB) LOS objects, 39% free, 21MB/35MB, paused 5.401ms total 73.333ms Background partial concurrent mark sweep GC freed 9126(463KB) AllocSpace objects, 3(156KB) LOS objects, 33% free, 31MB/47MB, paused 7.476ms total 48.278ms Background partial concurrent mark sweep GC freed 122448(9MB) AllocSpace objects, 0(0B) LOS objects, 33% free, 24MB/36MB, paused 3.021ms total 171.936ms Background partial concurrent mark sweep GC freed 34312(1960KB) AllocSpace objects, 1(20KB) LOS objects, 33% free, 23MB/35MB, paused 1.861ms total 110.321ms Background partial concurrent mark sweep GC freed 302696(15MB) AllocSpace objects, 5(124KB) LOS objects, 36% free, 28MB/44MB, paused 2.227ms total 140.014ms Background partial concurrent mark sweep GC freed 95715(7MB) AllocSpace objects, 4(80KB) LOS objects, 33% free, 24MB/36MB, paused 1.861ms total 106.079ms ... 12

Slide 13

Slide 13 text

パフォーマンス問題に対する考察 Nexus 7 2013という(今となっては)低スペック端末にとっては、 インスタンス生成が走りすぎているのかも知れない。 CoilのAsyncImageは色々インスタンス生成している 各AsyncImageごとにcoroutineを起動しているっぽい? その他、色々インスタンス生成している Modifierもメソッドチェーンする度に新しいインスタンスを生成しているっぽい 確かにAsyncImage無しだとかなりパフォーマンスが改善したので、 まずはCoil使うのをやめてみる。 ※ 一般的なケースでは、Coilがボトルネックになることは無いとおもいます!! 13

Slide 14

Slide 14 text

対策その1: Glideを使ってみる 14

Slide 15

Slide 15 text

対策その1: Glideを使ってみる Glide版の画像読み込みComposableをつくってみた。 以前はaccompanistでComposable版が提供されていた実績がある 昔からあるライブラリだから、使用するリソース的にも優しいはず 確かにパフォーマンスは改善したけど、まだカクついてる。 ソースコードのリンク: MainContentBSimpleAsyncImage 15

Slide 16

Slide 16 text

対策その2: さらにインスタンス生成を減らす 徹底的にインスタンス生成を減らす。 MaterialのCard使うのをやめる 内部で色々インスタンス生成しているため 一度つくったModifierはキャッシュする ソースコードのリンク: MainContentC 16

Slide 17

Slide 17 text

Modifierのキャッシュ class ModifierCacheHolder { private val map = mutableMapOf() @SuppressLint("ModifierFactoryExtensionFunction", "ComposableModifierFactory") @Composable fun getOrCreate( tag: String, creator: @Composable () -> Modifier, ): Modifier = map[tag] ?: creator.invoke().also { map[tag] = it } } ソースコードのリンク: ModifierCacheHolder 17

Slide 18

Slide 18 text

Modifierのキャッシュ val modifierCacheHolder = remember { ModifierCacheHolder() } Column( modifier = modifierCacheHolder.getOrCreate(tag = "MainRowRoot") { Modifier.padding(top = 8.dp) }, ) { ... } 18

Slide 19

Slide 19 text

Modifierのキャッシュ 基本的にModifierのインスタンスはStableなので、使い回しても大丈夫・・・なはず ただし、あんまり自信は無し 今のところ目に見えて不具合は起きていないので、たぶん大丈夫? ただし、Modifier.clickable()のインスタンスはキャッシュしちゃダメ 最初にセットしたクリックリスナーが使い回されてしまう clickable()の手前までのModifierのインスタンスなら、キャッシュしてOK 19

Slide 20

Slide 20 text

改善の成果をご覧下さい 20

Slide 21

Slide 21 text

21

Slide 22

Slide 22 text

残った課題に対する考察 22

Slide 23

Slide 23 text

残った課題に対する考察 高速なスクロールは、とてもカクつく 各行の描画にかかる時間が、そもそも1フレームを超えていそう そもそも高速スクロールで一瞬しか表示されないなら、描画をサボって良いのでは? 描画自体を遅延させてスキップ可能にしてみてはどうだろう? ソースコードのリンク: MainContentD 23

Slide 24

Slide 24 text

対策その3: 描画を遅延させてスキップ可能とする 24

Slide 25

Slide 25 text

対策その3: 描画を遅延させてスキップ可能とする @Composable fun LazyBox( modifier: Modifier = Modifier, delayMilliSec: Long, content: @Composable BoxScope.() -> Unit, ) { Box( modifier = modifier, ) { var showContent by remember { mutableStateOf(false) } LaunchedEffect(Unit) { withContext(Dispatchers.Default) { delay(delayMilliSec) showContent = true } } if (showContent) content.invoke(this) } } ソースコードのリンク: LazyBox 25

Slide 26

Slide 26 text

対策その3: 描画を遅延させてスキップ可能とする LazyBox( delayMilliSec = 10, placeHolder = { Spacer( modifier = modifierCacheHolder.getOrCreate(tag = "MainRowPlaceHolder") { Modifier.size(8.dp * 2 + 120.dp * 9f / 16) }, ) }, ) { LazyRow { items( items = data.rowIds, key = { it }, ) { rowId -> MainItemD( modifierCacheHolder = modifierCacheHolder, text = "${data.columnId}_$rowId", ) } } } 26

Slide 27

Slide 27 text

最終的にどうなったか 27

Slide 28

Slide 28 text

28

Slide 29

Slide 29 text

これまでのような対策の前に確認しておくと良い事 29

Slide 30

Slide 30 text

これまでのような対策の前に確認しておくと良い事 Compose Compiler Metricsのチェック https://github.com/androidx/androidx/blob/androidx- main/compose/compiler/design/compiler-metrics.md 特にListはそのままだとStable扱いにならないので要注意です 対策の例としては、@Immutableなdata classで包む Jetpack ComposeのComposable関数の引数に別のモジュールのクラスを使うときの注 意点 https://qiita.com/takahirom/items/6907e810d3661e19cfcf 30

Slide 31

Slide 31 text

他にも効果がありそうな事 31

Slide 32

Slide 32 text

他にも効果がありそうな事 Compose 1.2のcontentType LazyListでRecyclerViewのようにComposableをもっと効率的に再利用するための もの ただし、今回の例に適用してもあまり効果が感じられなかった 使い方が悪い?ちゃんと計測したら、実は改善されているのかも? Baseline Profiles あらかじめよく使うパスを計測しておくことで、それらのバイトコードからコンパ イルしてもらう、というもの? 効果が期待できそうだけど、まだ試せておらず 32

Slide 33

Slide 33 text

まとめと感想 33

Slide 34

Slide 34 text

まとめと感想 正直、やり過ぎたと思ってます たぶん、通常のアプリだとどうしても必要な場合だけLazyBoxいれる、ぐらいで良 いと思う そもそもBaseline Profilesで事足りるかも知れない 今時、Nexus 7 2013は流石に考慮しなくて良いと思う ただし、特にお手頃価格のタブレットはパフォーマンスが控えめな事もあるので、 実機で動作確認してみた方が良い 34