Compose原理十五之性能优化

在 Compose 声明式 UI 中,性能优化的核心理念是:尽可能地跳过不必要的重组。如果优化不当,微小的状态改变可能引起整个页面的大规模重构,从而导致卡顿。本文将介绍在业务开发中如何进行Compose性能优化。

1、缩小重组范围:抽取独立组件

Compose 的重组范围是基于读取状态的最小作用域(通常是最近的 @Composable 函数)。如果在一个庞大的组件中读取了某个频繁变化的状态,会导致整个大组件发生重组。将读取状态的部分抽取成独立的 @Composable 函数,可以有效缩小重组范围。

反面教材:

kotlin 复制代码
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    // 假设 name 是一个很少变化的状态,而 scrollOffset 是一个高频变化的状态
    val name by viewModel.name.collectAsState()
    val scrollOffset by viewModel.scrollOffset.collectAsState()

    Column {
        Text("Hello, $name") // 静态内容
        // ... 大量其他静态 UI ...
        
        // 仅仅因为这里读取了 scrollOffset,当它变化时,整个 ProfileScreen 都会重组!
        Text("Scroll Offset: $scrollOffset")
    }
}

正确做法:

kotlin 复制代码
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    val name by viewModel.name.collectAsState()

    Column {
        Text("Hello, $name")
        // ... 大量其他静态 UI ...
        
        // 将读取高频状态的 UI 抽取为独立的 Composable
        ScrollOffsetDisplay(viewModel)
    }
}

@Composable
fun ScrollOffsetDisplay(viewModel: ProfileViewModel) {
    // 状态读取被限制在这个小组件内部
    // 当 scrollOffset 变化时,只有 ScrollOffsetDisplay 会重组,ProfileScreen 不会!
    val scrollOffset by viewModel.scrollOffset.collectAsState()
    Text("Scroll Offset: $scrollOffset")
}
  • 内联函数不能作为最小重组范围,内联函数在编译期间会在调用处展开,重组的时候无法找到合适的调用入口。
  • 有返回值的函数不能作为最小重组范围,返回值需要在调用处使用,必须连同调用方一起重组。
  • 为了提升重组性能,可以将重组的组件放到一个空的非内联函数中。

2、驯服 "不稳定" 的状态

Compose 跳过重组的前提是:所有的参数都必须是稳定(Stable)的 。 什么是稳定的?基本类型(Int, String)、被 @Stable 或 @Immutable 标记的类。 什么是不稳定的?所有的集合接口(List, Map,因为 Compose 无法确定实现类是否可变)、var 属性暴露的普通类。

优化方案:

  • 使用只读集合 :不要传 List<T>,使用 Kotlinx Immutable 库的 ImmutableList<T>。

  • 给数据类打标签 :如果你确定一个 UI State 类在创建后不会内部修改,给它加上 @Immutable。

    kotlin 复制代码
    @Immutable // 告诉编译器:信任我,里面的数据不会变!
    data class UserState(val name: String, val tags: List<String>)
  • 解构传递 :不要把整个巨大的 ViewModel 或 Context 传给底层的叶子节点。只要你需要其中的一个 id,那就只传 id: String。

3、状态下放

这是 Compose 优化中最重要、最有效的一条规则! 如果一个状态仅仅是为了改变 UI 的绘制阶段(比如背景颜色、位移、透明度、滚动偏移量),千万不要在重组阶段读取它!

反面教材:

kotlin 复制代码
// scrollState.value 改变时,会导致整个 Column 重组!
val offset = scrollState.value
Column(Modifier.offset(y = offset.dp)) { ... } 

正确做法 (Lambda 延迟读取):

kotlin 复制代码
// Modifier.offset 接受一个 Lambda。
// state 的读取被推迟到了 Layout 阶段。
// scrollState 变化时,Column 不会发生重组 (Recomposition),只执行 Layout/Draw!性能提升巨大!
Column(Modifier.offset { IntOffset(0, scrollState.value.roundToInt()) }) { ... }

4、derivedStateOf:过滤高频状态

当你从一个高频变化的状态(比如列表滚动距离 scrollOffset)派生出一个低频变化的状态(比如是否显示"回到顶部"按钮 showFab)时,必须使用 derivedStateOf。

反面教材:

kotlin 复制代码
// scrollOffset 每次微小的改变(每秒几十次),都会导致重组
val showFab = scrollState.value > 1000 
if (showFab) { Fab() }

正确做法:

kotlin 复制代码
// 只有当 showFab 的布尔值发生真正的反转(false -> true 或 true -> false)时,才会触发重组
val showFab by remember {
    derivedStateOf { scrollState.value > 1000 }
}
if (showFab) { Fab() }

5、列表优化:LazyColumn & key

长列表是卡顿的重灾区。

  • 必须提供 key :如果不提供 key,当在列表顶部插入一条数据时,Compose 会把下面所有的数据全部推倒重做。提供了唯一 key,Compose 就能聪明地只是将已有节点向下平移。

    kotlin 复制代码
    LazyColumn {
        items(items = userList, key = { user -> user.id }) { user ->
            UserRow(user)
        }
    }
  • contentType (Compose 1.2+) :如果列表中有多种样式的卡片,指定 contentType 可以让 Compose 更加高效地在底层的组件池中复用相同类型的节点,极大地减少滑动时的创建开销。

6、remember 缓存昂贵的计算

如果必须在 Composable 中进行一些计算(比如过滤列表、格式化时间),一定要用 remember 把结果缓存起来。如果不包裹 remember,每次重组都会重新执行这部分计算逻辑。

反面教材:

kotlin 复制代码
@Composable
fun FilteredList(users: List<User>, keyword: String) {
    // 每次上层重组,或者仅仅是动画发生,都会重新遍历过滤整个 List!
    val filteredUsers = users.filter { it.name.contains(keyword) }
    LazyColumn { ... }
}

正确做法:

kotlin 复制代码
@Composable
fun FilteredList(users: List<User>, keyword: String) {
    // 只有当 users 或 keyword 发生变化时,才会重新执行过滤计算
    val filteredUsers = remember(users, keyword) {
        users.filter { it.name.contains(keyword) }
    }
    LazyColumn { ... }
}

7、避免频繁改变 Modifier 实例

在老版本的 Compose 中,Modifier 的创建和比较是有开销的,特别是在动画中。虽然现在的 Compose 编译器(Modifier Node 架构)优化了这部分,但在写动画时,依然应该优先使用基于 Lambda 的 Modifier 动画,而不是基于值的动画来重建 Modifier。

反面教材:

kotlin 复制代码
// 每一帧都会产生一个新的 Modifier 实例,导致不必要的 Recomposition 或 Layout
val padding by animateDpAsState(if (selected) 16.dp else 0.dp)
Box(modifier = Modifier.padding(padding))

推荐做法 (针对复杂/高频动画): 如果这是一个非常高频的自定义 Layout 或 Graphics 动画,尽量使用 Modifier.layout { ... } 或 Modifier.drawBehind { ... },将状态读取放在 Lambda 内部,避免重建 Modifier 链。

8、避免在 Composable 中执行耗时操作

Composable 函数必须是无副作用 且极速的。 绝对不要在组件中直接进行:

  • 解析大型 JSON
  • 读写 SharedPreferences / DataStore
  • 复杂的过滤和排序计算(应该放在 ViewModel 或放在 remember(xxx) { ... } 中)
相关推荐
晶捷软件4 小时前
从 IT 架构视角看 ERP 选型:用友、金蝶与鼎捷生态(含晶捷智能)的技术差异
架构·智能制造·erp·制造业信息化
@大迁世界4 小时前
Firefox 本地模式:localhost 再见了 ?
前端·架构
深蓝AI4 小时前
总榜第一不等于最划算:Intelligence Index v5 换代后,模型选型要算的三笔账
程序员·架构
天远数科4 小时前
零信任架构实战:基于天远企业股权变更构建自动化商业银行对公授信存续期股权异动与资产抽逃预警网关
运维·人工智能·架构·自动化
AI你一生一世5 小时前
当“暂停“成为架构决策:从签证系统停摆看高可用设计的边界
架构·系统架构·流量控制·幂等性·高可用设计·优雅降级·状态一致性
云宏信息10 小时前
超融合和存算分离怎么选?从性能、可靠性、扩展、成本四个维度讲述如何精准匹配架构选型 | 附企业降本轻量云方案
运维·服务器·架构·云计算
snpgroupcn10 小时前
能源行业SAP系统升级的技术约束分析:资产数据连续性与切换窗口的工程处理(上篇)
架构·sap
4SAPI11 小时前
会议智能处理大模型 API 生产选型指南:4SAPI、硅基流动与 OpenRouter 的架构权衡
人工智能·架构·自动化·gateway·php
长空任鸟飞_阿康11 小时前
如何设计支持 Skills 的 Agent?一套可扩展的四层架构
python·ai·架构·skills
得助智能-垂类大模型13 小时前
2026大模型知识库选型指南:多模态RAG、知识抽取与智能协同工程架构
架构