Compose性能优化:超越自己的实战指南

Jetpack Compose 性能优化方法论:从测量、组合成本到追踪工具的系统实践。
本文整理了 Compose 编译器运行时团队分享的性能优化方法论。文章强调性能测量必须在 release 模式、启用 R8 与 Baseline Profile 的前提下进行,否则数据严重失真。在组合阶段,每项操作都有成本,协程的启动与取消尤其昂贵,应根据需求选择成本最低的 effect API。Compose 1.13 引入的 computed state 在状态失效场景下比 derived state 快一倍以上,不确定时优先选用。文章还指出跳过 composable 本身有成本,避免失效比优化跳过更有效,strong skipping 可降低相等性比较开销。最后介绍了组合追踪与方法追踪的配合使用,以及将优化作为工程习惯的纪律要求。
Jetpack Compose 已成为 Android 开发的默认 UI 工具包,随之而来的问题也很直接:怎样让 Compose UI 跑得足够快?在一场技术分享中,来自 Compose 编译器运行时团队的两位工程师 Andrew 围绕性能测量、组合优化与工具链,给出了一套从宏观到微观的完整方法论。
为什么性能值得认真对待
性能常常在功能清单和动画设计之后才被想起,但用户对 App 的感知高度依赖它——动画一旦卡顿,评价立刻下滑。演讲者把各种指标定义为"代理指标",用来代表用户对界面的真实感受。换句话说,优化性能本质上是在优化体验。
在动手之前有三件事需要先做对:在 release 模式下测量性能,避免调试信息拖慢应用;启用 R8 等压缩工具,团队今年引入的一项优化就让性能提升了一倍;对启动指标使用 Baseline Profile,让启动时加载的类被预编译和预加载。演讲中给出的对比是:相比常规的可调试构建,这一组合能带来约 6 倍的差异。
这些基础工作完成后,才进入真正的性能循环:设定可重复的基准场景,用 Benchmark 采集数据,分析 profile,做出改动,再验证改动是否生效。
Baseline Profile 是一种以 AOT 方式预编译关键代码路径的机制。应用首次安装后默认以解释执行或 JIT 方式运行热点代码,启动阶段因此产生大量编译开销。Baseline Profile 通过提前记录典型用户旅程中的热点方法,将其随 APK 一同分发,使 ART 在安装时直接编译这些方法并预加载相关类,从而避开启动时的即时编译与类加载成本。它与 R8 的职责不同:R8 负责缩减和优化字节码,Baseline Profile 负责决定哪些代码被提前编译。两者叠加,才是演讲中“约 6 倍差异”的来源。
从 Perfetto 追踪看组合阶段
Perfetto trace 记录了设备上发生的一切——主线程、渲染线程、GPU 的工作都能看到。对于 Compose 开发者,主线程最值得关注,因为组合工作主要发生在这里。放大单帧后可以看到 composition、measure、layout 等切片,能直接读出每个阶段耗时。
性能基本功是"在正确的阶段做正确的事":如果动画只是改变方块颜色,就应当只触发 draw 阶段,而不是让 composition 和 layout 一起失效。但这次分享把焦点放得更深——在完成这些常规优化之后,组合内部还剩下什么。

组合里的每一项操作都有成本
一个容易被忽视的事实是:你发布的应用代码没有一行是免费的。读取状态、在组合中执行副作用,都有可感知的成本。在纳秒级尺度上,如果想真正改善性能,路径无非两条——消除大量廉价操作,或消除少量昂贵操作。协程属于后者,是值得优先审视的目标。
协程的成本不只在于启动,取消同样有代价,而取消大量发生在页面销毁、导航离开时——这恰恰是帧时间最紧张的场景。Compose 中协程最常通过 effect API 出现,而这些 API 本身就构成一条从高功能到低延迟的光谱:
LaunchedEffect:功能最全,适合需要协程作用域或挂起调用的场景,但成本最高DisposableEffect:适合有 setup/teardown 配对的过程- 带 key 的
SideEffect(Compose 1.12 引入):一次性事件的低成本替代 - 原生
SideEffect:最便宜,用于把外部状态同步进组合
迁移时要注意执行顺序保证:DisposableEffect 最先执行,然后是普通 SideEffect,LaunchedEffect 因涉及调度要到帧结束后才运行。依赖这些时序的代码必须先测试再迁移。如果多个 effect 使用相同的 key,把它们合并成一个也是减少开销的有效手段。
Effect API 的成本光谱背后是 Compose 运行时对副作用的不同管理方式。LaunchedEffect 需要创建协程作用域、向调度器提交任务,并在离开组合时取消协程,因此同时承担启动与取消两笔开销。DisposableEffect 只维护 setup/teardown 回调,不涉及协程调度,成本更低。SideEffect 在每次成功重组后同步执行,没有生命周期管理,因而最便宜。带 key 的 SideEffect 是 1.12 引入的折中方案,它只在 key 变化时执行一次性逻辑,避免为一次性事件引入完整协程。理解这一光谱,才能在功能需求与帧预算之间做出合理取舍。
组合追踪与重组追踪
普通 Perfetto 帧只能看到阶段耗时,看不到具体哪个 composable 在运行。组合追踪通过编译器插入的标记,把每个 composable 函数的耗时暴露在 trace 上,代价是有一定开销,追求最精确数据时仍应使用常规 Perfetto。
更进一步的 1.13 版本引入了重组追踪,它会标注每一次导致重组的状态读取和状态写入,并附带调用栈,可以直接对应到代码行。这让"为什么重组"从猜测变成可查证的事实。
computed state:避免无谓失效
调查之后常会发现,有些失效从一开始就不该发生。典型例子是"滚动到顶部"按钮:滚动状态可能每帧都在变,但真正关心的条件(是否已滚过阈值)很少变化。衍生状态(derivedStateOf)就是为这种场景设计的。
Compose 1.13 新增的 computed state 目标相同,机制不同:derived state 缓存计算结果直到依赖变化,computed state 每次读取都重新计算。听起来缓存更划算,实际未必——读快照状态本身并不便宜。
数据说明了取舍:首次读取时 computed state 约快 200 纳秒;在 Pixel 6 的特定基准条件下,第二次读取时 computed state 约慢 350 纳秒;而在真正关键的场景——状态被写入后重新计算(read after write)时,computed state 比 derived state 快一倍以上。

选择建议是:不确定时优先 computed state;只有当计算本身复杂(复杂算法、大对象分配、大量状态读取)或依赖对象相等性、能真正从缓存中获益时,才选 derived state。
derivedStateOf 与 computed state 的根本差异在于缓存策略与快照读取成本。derived state 在依赖未变化时直接返回缓存值,但判断依赖是否变化本身需要读取快照状态并做失效检查;computed state 不维护缓存,每次读取都重新计算,看似更贵,却省去了缓存有效性的维护开销。首次读取时 computed state 更快,正是因为跳过了缓存初始化。而在状态写入后的重新计算场景中,derived state 需要先使缓存失效再重算,computed state 则直接计算,差距被进一步拉大。这也解释了为什么“读快照状态本身并不便宜”是选择方案时的关键前提。
跳过不是终点,避免失效才是
社区里关于稳定性和跳过(skipping)的讨论很多,但演讲者明确提醒:跳过本身也有成本。为了支持跳过,运行时需要在首次组合时保存参数、在重组时更新记录,再逐个比较参数是否变化,最后只为了执行一个 if 判断。
一个经典反例是包含大量数据的滚动列表:修改最后一个联系人,理想情况只失效最后一项,实际上 lazy list 收到新的 content lambda 后会失效所有已组合的项。追踪显示,只有两项被真正跳过,其余都在完成昂贵的相等性比较后仍然执行。如果对象是稳定的,这种结构相等比较可能相当昂贵;由于数据被解构,同一份列表的比较还会做两次。
让编译器使用 strong skipping、走对象标识相等而非结构相等的路径,能显著降低这部分开销。更根本的原则是:与其想办法让 composable 跳过,不如从一开始就不让它失效。重组次数可以作为参考指标,但不该成为执念。

Strong skipping 是 Compose 编译器的一项优化模式,它改变了运行时的参数比较策略。传统模式下,只有被推断为稳定的参数才能参与跳过判断,不稳定参数会直接导致 composable 无法跳过。Strong skipping 放宽了这一限制,允许不稳定参数也参与跳过,并对 lambda 等类型采用对象标识相等而非结构相等进行比较。对象标识比较只需判断引用是否相同,避免了深层字段遍历,因此在数据类庞大或列表内容复杂时收益明显。这也正是演讲中“让编译器走对象标识相等路径”能显著降低相等性比较开销的原因。
方法追踪:填补组合追踪的空白
组合追踪只标注 composable 函数,trace 中那些空白区间里发生了什么并不清楚。此时需要方法追踪——Android Studio profiler 或 ADB 命令都能抓取,它记录每一次函数调用,包括编译器生成的代码、运行时逻辑和你自己的代码。代价是开销很高,根据调用树复杂度可达 10 到 20 倍。
典型用法是从组合追踪定位到可疑函数,再到方法追踪里查看细节。例如在 ScreenContent 的追踪中,大量时间消耗在 Composer.changed 上,而它又被 screenModel.equals 主导——这说明传入的是一个庞大的数据类,它的稳定性注解可能反而拖累了性能。这类问题只有方法追踪能直接暴露。
收尾:把优化当成工程习惯
演讲者最后给出的原则可以浓缩为几条:便宜且不失效的 composable 优于被跳过的 composable;设计时优先选择更小、更容易比较的参数;留意 API 成本乘以调用频率,以及可能在最糟糕时机出现的销毁成本。
优化也要讲纪律:先用基准测试做决策,再用 profiling 锁定目标区域,最后验证改动确实带来了预期结果。同时要意识到可维护性与性能之间存在权衡,不要为了榨取性能把自己逼进死角。
团队还把上述知识打包成了一个 Jetpack Compose 性能技能,通过 Android CLI 安装后可以让 AI agent 审计应用并给出优化建议,相关文档可在 developer.android.com 和 GitHub 仓库中找到。
相关推荐

合理使用制度为何对AI时代人人有益
从Hacker News讨论出发,解析合理使用(Fair Use)原则在AI训练与内容创作中的法律逻辑、争议焦点及其对创作者与技术生态的双重影响。

Salesforce 3.75亿美元收购Rune,AI物流技术补强国防业务
Salesforce 据报道以 3.75 亿美元收购国防科技初创公司 Rune,这家 AI 物流公司的技术将用于补强 Salesforce 的 Missionforce 国防平台,也延续了 Salesforce 近期的并购节奏。

Mistral Large 4开源万亿参数,微软6万员工自愿离职
Mistral Large 4 开源万亿参数多模态模型,编码智能体评测得分 48;闪迪财报显示 AI 推理重塑 NAND 需求;谷歌 Gemini 上线应用内文件编辑,OpenAI 上调 Codex 额度,微软向 6 万员工提供自愿离职方案。