Compose 性能优化
本教程共 100 篇 · 第 56 篇 · 更新于 2026-07-28 · 约 8 分钟阅读
56. Compose 性能优化
本节目标:理解重组的性能影响,学会用稳定类型、derivedStateOf、key 等手段减少不必要的重组,写出高性能 Compose 界面。
重组是性能的关键
Compose 的性能核心在于少重组。每次重组都要重新执行函数、比较参数、更新界面树。重组太频繁会导致卡顿掉帧。
Compose 会做智能跳过:如果函数的参数没变,就跳过重组。但前提是—参数能被正确比较。
稳定类型(Stable)
Compose 判断参数是否变化,依赖类型的「稳定性」:
- 稳定类型:
String、基本类型(Int、Boolean 等)、@Stable注解的类。Compose 能确定两个实例相等时内容相同,可以安全跳过。 - 不稳定类型:
List、Map、普通data class包含集合字段等。Compose 不确定内容是否变了,只能保守地重组。
// 稳定:基本类型
@Composable
fun NameText(name: String) { // String 是稳定的
Text(name)
}
// 不稳定:List 参数
@Composable
fun ItemList(items: List<String>) { // List 不稳定,每次都可能重组
items.forEach { Text(it) }
}
Warning
List<String>是不稳定类型。即使列表内容没变,Compose 也可能重组这个函数。这是性能问题的常见来源。
用 ImmutableList 解决
// 用 @Immutable 注解标记不可变集合
@Immutable
data class ItemList(val items: List<String>)
@Composable
fun DisplayItems(itemList: ItemList) {
// 现在可以安全跳过了
itemList.items.forEach { Text(it) }
}
// 或用 Kotlinx Collections Immtable
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
@Composable
fun DisplayItems(items: ImmutableList<String>) {
items.forEach { Text(it) }
}
Tip
@Immutable是你向 Compose 的承诺:这个类的内容不会变。Compose 信任你,参数不变就跳过。但如果你的类其实可变却标了@Immutable,会导致界面不更新—别乱标。
derivedStateOf 减少重组
当状态频繁变化但派生结果很少变化时,用 derivedStateOf:
// 坏例子:滚动位置每帧都变,导致 if 判断每帧重组
@Composable
fun ScrollButton(scrollState: ScrollState) {
if (scrollState.value > 1000) { // 每帧都重新评估
Button(onClick = { }) { Text("回顶") }
}
}
// 好例子:只有跨过阈值时才重组
@Composable
fun ScrollButton(scrollState: ScrollState) {
val showButton by remember {
derivedStateOf { scrollState.value > 1000 }
}
if (showButton) { // 只在 true/false 切换时重组
Button(onClick = { }) { Text("回顶") }
}
}
Note
- 滚动时
scrollState.value每帧从 0 变到 2000,但> 1000只有在跨过 1000 时才从 false 变 true。derivedStateOf让只有布尔值变化时才触发重组,而不是每帧。
列表加 key
第 45 章提过,LazyColumn 的 items 一定要加 key:
LazyColumn {
items(items = list, key = { it.id }) { item ->
ItemRow(item)
}
}
没有 key,列表数据变化时 Compose 按位置匹配,可能把错误的 item 内容显示在错误的位置。加了 key 才能正确追踪每一项,避免不必要的重组。
避免在重组中创建对象
// 坏例子:每次重组都创建新的 lambda
@Composable
fun BadExample(onClick: () -> Unit) {
Button(onClick = { onClick() }) { // 每次重组都创建新 lambda
Text("点击")
}
}
// 好例子:lambda 不依赖状态时提到外面
@Composable
fun GoodExample(onClick: () -> Unit) {
Button(onClick = onClick) { // 直接用外部 lambda
Text("点击")
}
}
延迟读取状态
状态读取越晚越好,读取范围越小越好:
// 坏例子:在父函数读状态,整个函数都重组
@Composable
fun BadList(items: List<Item>, viewModel: ViewModel) {
val selectedId by viewModel.selectedId.collectAsState() // 这里读
LazyColumn {
items(items) { item ->
// 即使只有选中项变了,整个 BadList 都重组
ItemRow(item, isSelected = item.id == selectedId)
}
}
}
// 好例子:在子函数读状态,只重组变化的项
@Composable
fun GoodList(items: List<Item>, viewModel: ViewModel) {
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(item, viewModel) // 把 viewModel 传下去
}
}
}
@Composable
fun ItemRow(item: Item, viewModel: ViewModel) {
val selectedId by viewModel.selectedId.collectAsState() // 在这里读
ItemContent(item, isSelected = item.id == selectedId)
}
Tip
- 原则是「状态读取尽量靠近使用它的地方」。这样状态变化时只重组读它的那个小组件,而不是整个大组件。这是 Compose 性能优化最重要的一条。
remember 缓存计算结果
@Composable
fun DateText(timestamp: Long) {
val dateText = remember(timestamp) {
SimpleDateFormat("yyyy-MM-dd").format(Date(timestamp))
}
Text(dateText)
}
remember(timestamp) 只在 timestamp 变化时才重新格式化。不变时直接用缓存值,避免每次重组都做日期格式化。
性能分析工具
Layout Inspector
Android Studio 的 Layout Inspector 可以查看 Compose 的重组情况:
- 打开 View > Tool Windows > Layout Inspector
- 连接设备,选择应用
- 切换到 Composition 选项卡
- 开启「Show Recomposition Counts」
重组次数多的组件会标红,帮你定位性能热点。
Compose Compiler Metrics
在 gradle.properties 里开启编译器报告:
compose.compiler.metrics=true
compose.compiler.reports=true
构建后会在 build/compose-metrics/ 生成报告,显示哪些函数是 restartable、skippable,哪些类是 stable、unstable。
Note
- 一个理想的可组合函数应该是
skippable(参数不变时可跳过)+restartable(可重组)。报告里标unstable的参数就是优化重点。
常见性能陷阱
| 陷阱 | 后果 | 解决 |
|---|---|---|
List 参数不稳定 | 不必要重组 | 用 @Immutable 或 ImmutableList |
| 没加 key 的列表 | 数据变化时错乱 | 加 key = { it.id } |
| 在顶层读状态 | 大范围重组 | 状态读取靠近使用处 |
| 重组中创建对象 | 频繁 GC | 用 remember 缓存 |
derivedStateOf 缺失 | 频繁状态触发重组 | 用 derivedStateOf 包裹派生计算 |
小结
Compose 性能优化的核心是「减少不必要的重组」。用稳定类型让 Compose 能安全跳过,用 derivedStateOf 减少派生状态的重组,用 key 让列表正确追踪项,把状态读取延迟到最靠近使用的位置,用 remember 缓存计算结果。配合 Layout Inspector 和编译器报告定位热点。下一部分我们回到传统 View 体系的学习。