22、性能优化:重组优化(remember、derivedStateOf),列表性能(key、itemContentType),布局性能(IntrinsicSize、SubcomposeLayout)
性能优化这个话题,说实话,很多初学者容易忽略。但等你真正上生产环境,用户手机一卡,你就知道疼了。
Jetpack Compose 的渲染机制和传统 View 系统不太一样。它靠的是「重组」——也就是状态变了,框架自动重新执行可组合函数。这个机制本身很优雅,但用不好,就会造成大量不必要的重组,拖垮 UI 帧率。
今天我就把 Compose 性能优化的三个核心方向掰开揉碎讲清楚:重组优化、列表性能、布局性能。每个点我都会结合项目里的真实踩坑经历来说。
核心原则:Compose 性能优化的本质,就是减少不必要的重组,降低布局测量次数,让列表高效复用。
一、重组优化:别让 Compose 白干活
重组是 Compose 的核心机制。但问题在于,重组是「传染性」的——父组件重组,子组件跟着重绘。如果你不加以控制,一个微小的状态变化可能引发整棵组件树重新执行。
1.1 remember:缓存计算结果
remember 的作用很简单:把计算结果缓存起来,避免每次重组都重新计算。我见过不少新手把复杂计算直接写在可组合函数里,结果每次重组都算一遍,卡得不行。
// ❌ 错误写法:每次重组都重新计算
@Composable
fun ExpensiveComponent(items: List<String>) {
val filtered = items.filter { it.startsWith("A") } // 每次重组都执行
// ...
}
// ✅ 正确写法:用 remember 缓存
@Composable
fun ExpensiveComponent(items: List<String>) {
val filtered = remember(items) { items.filter { it.startsWith("A") } }
// ...
}
我的习惯:只要涉及集合操作、字符串拼接、数学计算,一律用 remember 包起来。养成这个习惯后,我几乎没再遇到过「莫名其妙卡顿」的问题。
remember 的第二个参数是「键」。只有键发生变化时,才会重新计算。如果你传了空键列表 remember { ... },那这个值在整个组合生命周期内都不会变。
1.2 derivedStateOf:派生状态的正确姿势
有时候你需要根据多个状态计算出一个「派生值」。比如:根据用户名和用户等级,拼接显示文本。这时候用 derivedStateOf 比用 remember 更合适。
// 用 derivedStateOf 派生状态
@Composable
fun UserGreeting(userName: String, userLevel: Int) {
val displayText by remember {
derivedStateOf {
if (userLevel > 5) "尊贵的 $userName" else "你好,$userName"
}
}
Text(text = displayText)
}
为什么不用 remember?因为 remember 需要你手动指定依赖键,而 derivedStateOf 会自动追踪内部读取的所有状态。说白了,它更智能,也更安全。
我曾经踩过的坑:在列表中使用 derivedStateOf 时,忘记用 remember 包裹。结果每次重组都新建一个 derivedStateOf 对象,反而更慢了。记住:derivedStateOf 必须配合 remember 使用。
二、列表性能:LazyColumn 的正确打开方式
列表是 App 中最常见的场景。LazyColumn 虽然已经做了视图复用,但如果你不告诉它「哪些 item 是相同的」,它还是会做很多无用功。
2.1 key:给每个 item 一个身份证
key 参数的作用是让 Compose 知道「这个 item 是谁」。没有 key,Compose 只能靠位置来匹配——这会导致滚动时 item 内容闪烁、动画错乱。
// ✅ 正确:使用唯一 ID 作为 key
LazyColumn {
items(items = userList, key = { it.id }) { user ->
UserItem(user)
}
}
// ❌ 错误:没有 key,或者用 index 作为 key
LazyColumn {
itemsIndexed(userList) { index, user ->
UserItem(user) // 位置变化时,内容可能错乱
}
}
你想想看,如果列表数据变了——比如删除了第 3 项——没有 key 的话,Compose 会认为第 4 项变成了新的第 3 项,然后重新组合。有了 key,它就知道「哦,第 4 项还是那个 id,只是位置变了」,直接复用。
2.2 itemContentType:告诉 Compose item 的类型
如果你的列表里有多种类型的 item(比如:文本、图片、视频),一定要用 itemContentType 告诉 Compose。这样 Compose 就能对不同类型分别做复用,避免类型混乱。
LazyColumn {
items(
items = mixedList,
key = { it.id },
itemContentType = { it.type } // 告诉 Compose 类型
) { item ->
when (item.type) {
"text" -> TextItem(item)
"image" -> ImageItem(item)
"video" -> VideoItem(item)
}
}
}
性能提升数据:我在一个社交 App 项目中,给混合 Feed 流加上 key 和 itemContentType 后,列表滑动帧率从 35fps 提升到了 55fps。效果立竿见影。
三、布局性能:让测量更聪明
Compose 的布局阶段会执行多次测量。如果布局嵌套太深,或者测量逻辑复杂,就会拖慢 UI 线程。
3.1 IntrinsicSize:提前告诉父布局「我有多大」
有些场景下,父布局需要知道子布局的「固有尺寸」才能做决策。比如:一个 Column 里的两个子组件,一个要撑满剩余空间,另一个要自适应高度。这时候用 IntrinsicSize 可以避免二次测量。
Column(
modifier = Modifier.height(IntrinsicSize.Min) // 告诉 Column:高度由子组件决定
) {
Text("自适应内容", Modifier.weight(1f))
Text("固定底部")
}
没有 IntrinsicSize 的话,Compose 需要先测量子组件,再调整父布局,再重新测量——多了一次测量循环。用上它,一次测量就搞定。
我建议:只在确实需要「子组件决定父布局尺寸」的场景下使用 IntrinsicSize。滥用反而会增加测量开销。
3.2 SubcomposeLayout:按需组合子组件
SubcomposeLayout 是 Compose 提供的一个高级布局工具。它允许你在布局阶段「按需组合」子组件——也就是说,只有真正需要显示的子组件才会被组合。
举个例子:一个横向滚动的列表,你只想组合当前可见的 item。用 SubcomposeLayout 可以做到。
@Composable
fun LazyHorizontalGrid(
items: List<Item>,
modifier: Modifier = Modifier
) {
SubcomposeLayout(modifier = modifier) { constraints ->
// 根据 constraints 计算可见范围
val visibleItems = calculateVisibleItems(constraints, items)
// 只组合可见的 item
val placeables = visibleItems.map { item ->
subcompose(item.id) { ItemView(item) }[0].measure(constraints)
}
// 布局
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { it.placeRelative(0, 0) }
}
}
}
注意:SubcomposeLayout 是高级 API,普通场景用 LazyColumn 就够了。我只有在自定义「瀑布流」「时间轴」等复杂布局时才会用到它。用不好反而会引入 bug。
四、总结:性能优化的心法
说了这么多,其实核心就三句话:
- 重组优化:用
remember缓存计算结果,用derivedStateOf派生状态,减少不必要的重组。 - 列表性能:给每个 item 设置
key,给不同类型设置itemContentType,让 LazyColumn 高效复用。 - 布局性能:用
IntrinsicSize减少测量次数,用SubcomposeLayout按需组合子组件。
嗯,这些技巧我在多个项目里验证过。只要你按照这个思路去优化,Compose 应用的性能绝对不会差。记住:性能优化不是一蹴而就的,而是在开发过程中一点点积累的习惯。