Jetpack Composeでパフォーマンスが良くない書き方を避ける

基本的には「**早すぎた最適化は避けましょう**」が前提にはあるのですが、パフォーマンス的に気をつけた方がいい書き方を紹介します。


## 0. Jetpack Compose の最新バージョンを使用する
書き方の前に、Jetpack Compose は新しいバージョンほどパフォーマンス改善が入っているので、極力新しいバージョンを使いましょう。
- Modifier の Node 化
- Compose 1.7.0 で入る strong skip mode

## 1. derivedStateOf は必要以上に使わない
変更のバッファリングをするためコストがかかる処理なので、頻繁に値が変わるけど結果が変わらなくて recompose を避けたい時だけに使用しましょう。
単純な値変更による recompose で問題ない時は `derivedStateOf` を使う必要はないです。

```kotlin
 tabs.forEachIndexed { index, tab ->
    // ❌
    val isSelected = remember { 
        derivedStateOf { pagerState.currentPage == index}
    }
    // ⭕️
    val isSelected = currentPage == index
    Tab(
        selected = isSelected,
        ...
    )
 }

 val items by viewModel.items.collectAsState()
 // ❌
 val itemCount by remember {
     derivedStateOf { items.count }
 }
 // ⭕️
 val itemCount = items.count 
```


## 2. 頻繁な LaunchedEffect の再起動を避ける

coroutines のキャンセルと再起動が行われるので `LaunchedEffect` の key に頻繁に変わりうる値を設定しないようにしましょう。
頻繁に値が変わるものを使う場合は `snapshotFlow` で購読しましょう。

```kotlin
 // ❌
 LaunchedEffect(pagerState.currentPage) {
    // Do anything
 }
 //　⭕️
 LaunchedEffect(pagerState) {
    snapshotFlow { pagerState.currentPage }
        .collect {
            // Do anything
        }
 }
```

## 3. List の操作はキャッシュさせる
Composable fun の内部で直接 List の操作を行うと recompose 時に List の操作が都度行われてしまうので避けましょう。
`remember {}` でキャッシュさせるか、ViewModel などですでに操作された後の状態で保持するようにしましょう。

```kotlin
 // ❌
 val labelIds = labelList.map { it.id }
 // ⭕️
 val labelIds = remember(labelList) {
  labelList.map { it.id }
}
```

## 4. Lazy 系のスコープ内で State にアクセスしない
`LazyListScope` 内で State にアクセスすると State の更新で `LazyListScope` が何度も作り直されることになり、Lazy 系が頻繁に recompose されるのでパフォーマンスが悪くなります。
`LazyListScope` 内では状態が変わるものを参照しないようにしましょう。

```kotlin
// ❌
val listState = rememberLazyListState()
LazyColumn(state = listState) {
  items(items) {
      Text(text = it)
  }
  if (listState.firstVisibleItemIndex + listState.layoutInfo
          .visibleItemsInfo.size == listState.layoutInfo.totalItemsCount) {
      // Do anything
  }
}

// ⭕️
val isReachedBottom by remember {
  derivedStateOf {
      listState.firstVisibleItemIndex + listState.layoutInfo
          .visibleItemsInfo.size == listState.layoutInfo.totalItemsCount
  }
}
LaunchedEffect(isReachedBottom) {
  if (isReached) {
      // Do anything
  }
}
LazyColumn(state = listState) {
  items(items) {
      Text(text = it)
  }
}
```


## 5. Log による recompose を避ける
Log の文字列で State 等を参照していると State の変更で文字列が生成されることによる recompose が発生するので `SideEffect` で囲むようにしましょう。

```kotlin
// ❌
Log.d(TAG, "List recompose ${listState.firstVisibleItemIndex}")
// ⭕️
SideEffect {
    Log.d(TAG, "List recompose ${listState.firstVisibleItemIndex}")
}
```


## 6. アニメーションや状態が頻繁に変わるものを参照するときは遅延させる
アニメーションや State のような状態が頻繁に変わりうるものを扱う場合には必要以上に recompose されないように遅延させましょう。

```kotlin
val color by animateColorBetween(Color.Cyan, Color.Magenta)

// ❌
Box(modifier = Modifier.background(color))
// ⭕️
Box(modifier = Modifier.drawBehind { drawRect(color) })


val alpha by animateFloatAsState(
    targetValue = if (isVisible) { 1f } else { 0f },
)
// ❌
Box(modifier = Modifier.alpha(alpha))
// ⭕️
Box(modifier = Modifier.graphicsLayer { this.alpha = alpha })
```



## 参考

- https://developer.android.com/jetpack/compose/performance
- https://developer.android.com/jetpack/compose/side-effects
- https://developer.android.com/jetpack/compose/phases