27、性能优化:remember、derivedStateOf、key 的使用,避免重组

性能优化这个话题,说实话,在 Compose 里绕不开。

我见过不少项目,功能都跑通了,界面也好看,但一滑动列表就卡成 PPT。问题出在哪?说白了,就是重组次数太多了。Compose 的声明式 UI 虽然爽,但如果你不管住重组,它就会像脱缰的野马一样到处跑。

今天我们就来聊聊三个核心工具:rememberderivedStateOfkey。用好它们,你的界面就能从「勉强能用」变成「丝般顺滑」。

重组到底是个啥?

先简单过一下概念。Compose 里,只要状态变了,跟这个状态相关的 Composable 函数就会重新执行。这个过程就叫重组。

重组本身不是坏事,它是 Compose 的核心理念。但问题是,如果重组范围太大、频率太高,UI 线程就会被塞满,掉帧就来了。

嗯,这里要注意:重组不是整个界面重绘,而是只重组那些「依赖了变化状态」的组件。但如果你代码写得糙,一个按钮的点击就能让整个页面重跑一遍,那就尴尬了。

核心原则: 让重组范围尽可能小,让计算尽可能少。

remember:把值「记住」

remember 是 Compose 里最基础的缓存工具。它的作用很简单:在重组时,如果 key 没变,就不重新计算,直接拿上次的结果。

我举个例子。假设你有一个函数,每次调用都生成一个随机数:

@Composable
fun RandomNumberDisplay() {
    val number = Random.nextInt(100)
    Text("随机数:$number")
}

这段代码有什么问题?每次重组,Random.nextInt 都会重新执行,数字一直在变。这显然不是我们想要的。

加上 remember 就好了:

@Composable
fun RandomNumberDisplay() {
    val number = remember { Random.nextInt(100) }
    Text("随机数:$number")
}

这样,只有首次进入组合时才会生成随机数,后续重组直接复用。

我的习惯: 只要涉及对象创建、计算开销大的操作,我都会用 remember 包一层。比如创建列表适配器、格式化日期、解析 JSON 等。

remember 的 key 参数

remember 还可以传一个 key。当 key 变化时,缓存会失效,重新计算。

我在项目中遇到过这样一个场景:一个详情页,根据传入的 userId 加载用户信息。如果 userId 变了,必须重新加载,不能复用旧数据。

@Composable
fun UserProfile(userId: String) {
    val userInfo = remember(key1 = userId) {
        fetchUserFromNetwork(userId)
    }
    // 展示用户信息
}

这里 key 就是 userId。当用户切换时,userId 变了,remember 发现 key 不匹配,就会重新执行 fetchUserFromNetwork

注意: key 不要传一个会频繁变化的值,否则缓存就形同虚设了。我曾经见过有人把当前时间戳当 key 传进去……那跟没加 remember 没区别。

derivedStateOf:派生状态

这个工具很多人容易忽略,但它非常实用。

derivedStateOf 的作用是:根据一个或多个状态,计算出一个新的值。而且它只在「真正需要」的时候才重新计算。

你想想看,如果你有一个列表,里面有一万个条目,你只想统计其中「已完成」的数量。如果每次列表变化都重新遍历一遍,那性能就炸了。

derivedStateOf 可以这样写:

@Composable
fun TaskList(tasks: List<Task>) {
    val completedCount = remember {
        derivedStateOf {
            tasks.count { it.isCompleted }
        }
    }

    Text("已完成:${completedCount.value}")
}

这里 derivedStateOf 会监听 tasks 的变化。只有当 tasks 列表本身发生变化时,才会重新计算 completedCount。如果只是界面重组但数据没变,它不会做无用功。

一句话总结: derivedStateOf 适合做「从现有状态推导出新状态」的场景,比如过滤、排序、统计。

key:帮 Compose 识别「谁是谁」

这个 key 不是 remember 的那个 key,而是 Composable 的 key 参数。

Compose 通过位置和类型来识别组件。但如果列表中的元素位置变了,或者你用了 animateContentSize 这类动画,Compose 可能会搞混,导致不该重组的重组了,或者动画效果错乱。

举个例子。一个可拖拽排序的列表,如果你不给每个条目加 key,拖拽后 Compose 会认为「位置 0 的条目还是原来那个」,结果状态全乱了。

@Composable
fun SortableList(items: List<Item>) {
    LazyColumn {
        items(items, key = { it.id }) { item ->
            ItemRow(item)
        }
    }
}

加上 key = { it.id } 后,Compose 就能准确识别每个条目。即使位置变了,它也知道「哦,这个条目只是挪了个位置,内容没变,不用重组」。这能省下大量不必要的重组开销。

避坑指南: 我曾经在一个聊天列表里忘了加 key,结果用户发完消息后,消息列表闪了一下,输入框还丢了焦点。加上 key 后问题立刻消失。所以,列表一定要加 key,尤其是可变的列表。

知识体系总览

下面这张图帮你理清这三个工具的关系:

Compose 性能优化三件套 remember 缓存计算结果 避免重复创建对象 支持 key 控制失效 derivedStateOf 派生状态计算 按需重新计算 适合过滤/统计/排序 key 唯一标识组件 避免重组错乱 列表必须加 key 核心目标:减少不必要的重组 remember → 缓存值,避免重复计算 derivedStateOf → 按需派生,避免无谓遍历 key → 精准识别,避免组件错乱

避免重组的实战技巧

光知道工具还不够,得知道什么时候用。我总结几个实战场景:

1. 把「不变的内容」抽成独立 Composable

如果一个 Composable 里既有变化的部分,又有不变的部分,就把不变的部分抽出来。这样变化只影响小范围,不变的部分不会跟着重组。

@Composable
fun ExpensiveHeader() {
    // 这里很重,但很少变化
    Image(...)
    Text("固定标题")
}

@Composable
fun DynamicContent(count: Int) {
    Text("当前计数:$count")
}

2. lambda 表达式要小心

每次重组,lambda 都会创建新对象。如果 lambda 作为参数传给子组件,子组件就会因为「参数变了」而跟着重组。

解决办法:用 remember 包一下,或者用 derivedStateOf 把计算结果传下去。

// 不好的写法
Button(onClick = { viewModel.onClick() })

// 好的写法
val onClick = remember { { viewModel.onClick() } }
Button(onClick = onClick)

3. 列表用 LazyColumn + key

这个前面说过了,但值得再强调一遍。LazyColumn 本身已经做了按需渲染,但如果你不加 key,它就无法准确判断哪些条目需要重组。尤其是列表有增删改操作时,key 是必须的。

我的经验: 只要列表里的数据有唯一 ID,我就一定把 ID 作为 key 传进去。哪怕列表只有 5 个条目,我也这么做。养成习惯,就不会踩坑。

总结

性能优化这件事,说白了就是「少干活」。Compose 已经帮我们做了很多,但如果你不注意,它也会做很多无用功。

remember 帮你缓存计算结果,derivedStateOf 帮你按需派生状态,key 帮你精准识别组件。这三个工具用好了,你的 Compose 应用就能跑得又快又稳。

嗯,今天就聊到这。记住一句话:重组不是敌人,无意义的重组才是。


公众号:蓝海资料掘金营,微信 deep3321