基本的には「早すぎた最適化は避けましょう」が前提にはあるのですが、パフォーマンス的に気をつけた方がいい書き方を紹介します。
0. Jetpack Compose の最新バージョンを使用する
書き方の前に、Jetpack Compose は新しいバージョンほどパフォーマンス改善が入っているので、極力新しいバージョンを使いましょう。
- Modifier の Node 化
- Compose 1.7.0 で入る strong skip mode
1. derivedStateOf は必要以上に使わない
変更のバッファリングをするためコストがかかる処理なので、頻繁に値が変わるけど結果が変わらなくて recompose を避けたい時だけに使用しましょう。
単純な値変更による recompose で問題ない時は derivedStateOf
を使う必要はないです。
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
で購読しましょう。
// ❌
LaunchedEffect(pagerState.currentPage) {
// Do anything
}
// ⭕️
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }
.collect {
// Do anything
}
}
3. List の操作はキャッシュさせる
Composable fun の内部で直接 List の操作を行うと recompose 時に List の操作が都度行われてしまうので避けましょう。
remember {}
でキャッシュさせるか、ViewModel などですでに操作された後の状態で保持するようにしましょう。
// ❌
val labelIds = labelList.map { it.id }
// ⭕️
val labelIds = remember(labelList) {
labelList.map { it.id }
}
4. Lazy 系のスコープ内で State にアクセスしない
LazyListScope
内で State にアクセスすると State の更新で LazyListScope
が何度も作り直されることになり、Lazy 系が頻繁に recompose されるのでパフォーマンスが悪くなります。
LazyListScope
内では状態が変わるものを参照しないようにしましょう。
// ❌
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
で囲むようにしましょう。
// ❌
Log.d(TAG, "List recompose ${listState.firstVisibleItemIndex}")
// ⭕️
SideEffect {
Log.d(TAG, "List recompose ${listState.firstVisibleItemIndex}")
}
6. アニメーションや状態が頻繁に変わるものを参照するときは遅延させる
アニメーションや State のような状態が頻繁に変わりうるものを扱う場合には必要以上に recompose されないように遅延させましょう。
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 })