mcpField
J
jetpack-compose-performance

jetpack-compose-performance

github.com/android开发10.0k378 次安装2 天前更新
代码仓库

关于 jetpack-compose-performance

jetpack-compose-performance 是用于改善 Jetpack Compose 性能的代码审查技能。它依据细则检查 Composable、修饰符和惰性布局模型,并要求在报告或修改问题前阅读对应参考资料。

主要功能

  1. 01审查七类优化问题
  2. 02检查状态读取与稳定性
  3. 03检查布局测量和放置
  4. 04审视惰性布局数据模型
  5. 05修改前必须阅读参考文件
  6. 06强调发布构建与基准测试

适用场景

  1. 01审查 Compose 代码中的性能问题
  2. 02排查重组或滚动开销
  3. 03指导布局与状态处理的修复

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/android/skills --skill jetpack-compose-performance

安装后在 Claude Code 中输入/jetpack-compose-performance确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
animation.md
effects-and-threading.md
graphics-performance.md
layouts.md
lazy-layouts.md
state-and-stability.md
references/android/develop/ui/compose/
custom-modifiers.md
references/android/develop/ui/compose/performance/
backwards-write.md
references/android/develop/ui/compose/performance/stability/
strongskipping.md

详细介绍

基础前提

在对可组合项进行微优化之前,请确保已启用标准生产环境标志:

  1. 发布构建:切勿分析或基准测试可调试构建;ART 运行时验证会严重降低性能。
  2. R8 混淆:保持启用 R8,以优化字节码和协程调度。
  3. 基线配置文件:为关键用户流程(冷启动、关键滚动信息流)生成基线配置文件,让 ART VM 提前预编译并预加载类。

优化审计工作流

  1. 全面扫描:根据以下部分中的全部 7 个核心类别扫描目标代码。根据所有检查清单规则评估每个 @Composable 函数、修饰符链和惰性布局数据模型,不要在发现第一个问题后就停止。
  2. 标记或编辑前必须阅读参考资料:每当某个文件符合类别 1--7 中的某条规则时,你必须先打开并阅读对应的 references/*.md 文件,了解经认可的修复模式和完整的范围限制,之后才能报告发现或编辑代码。编写修复方案时,切勿只依赖摘要要点。

1. 状态、快照和稳定性(references/state-and-stability.md)

强制要求:在标记或修复此类别中的任何问题之前,你必须阅读 references/state-and-stability.md。

  • 延迟到后续阶段(Lambdas):将频繁变化的状态读取包装在 lambda 中(例如 Modifier.offset 或 Modifier.graphicsLayer),使读取发生在 Layout 或 Draw 阶段,而不是 Composition 阶段。
  • 禁止反向写入(验证读写顺序):绝不要在后续阶段修改状态,也不要在同一 composition pass 中读取状态后再修改它(Read -> Write,或 Composition Read -> Layout or Draw Write)。避免跨阶段反向写入,以免出现首帧跳变、卡顿和无限重组循环(反向写入)。不要将 composable 开始处的状态赋值(Write -> Read 或 Composition Write -> Draw Read)标记为反向写入——反向写入严格来说要求在写入之前读取该状态。
  • 避免在 Composition 阶段进行冗余状态包装(Write -> Read):不要仅为了在 composition 开始处立即覆盖同步值,然后再读取它们,而将其包装在 remember { mutableStateOf(...) } 中(例如,var path by remember { mutableStateOf(Path()) }; path = viewModel.getPath())。这虽然不属于反向写入,但会增加不必要的 slot table 和 MutableState 快照开销(如果之后在赋值之前添加读取,还可能变成反向写入);应使用普通局部 val,或直接在目标阶段读取该值(例如 Modifier.drawWithCache)。
  • 显式指定 remember 键和 Slot 开销:始终将 remember 中引用的所有输入变量和 composable 参数作为计算键传入。请记住,remember 会在 slot table 中存储 keys + 1 个对象,并在每次重组时检查键是否相等;只有在分配开销较大或必须保留对象标识时才使用 remember。包装 derivedStateOf 时,不要将快照状态值/属性(例如 state.value)作为键传入(如果 State 实例的引用可能变化,则传入该实例本身没有问题)。
  • computedStateOf 与 derivedStateOf(Compose 1.13+):在 Compose 1.13+ 中,对于简单条件检查和从快速变化的输入派生的筛选状态(例如 scrollState.value > 0),使用 computedStateOf,其失效速度明显快于 derivedStateOf(在 Compose < 1.13 中,使用 remember { derivedStateOf { ... } })。将 derivedStateOf 留给能从多次读取缓存中受益的复杂计算,或在每次 composition 中读取频率足够高、可抵消失效开销的简单计算(在读取之间不发生失效时,大约达到 10 / numberOfStatesReadPerCalculation 次)。
  • 同步派生状态:检查是否有异步 produceState + snapshotFlow 块在观察 Compose 状态(例如滚动位置或集合大小);将其替换为同步的 remember { derivedStateOf { ... } }(Compose 1.13+ 中使用 computedStateOf)。
  • 简单基本类型:对于简单的布尔表达式和属性查找,优先使用普通自定义 Kotlin getter(get()),而不是 derivedStateOf 或 computedStateOf。
  • 集合状态跟踪:对于整个列表的更新,使用标准的 mutableListOf() 和 mutableStateOf(list, neverEqualPolicy()),并通过实际赋值触发更新,而不是使用 mutableStateListOf()。
  • 快照集合:重复查询或迭代时,使用 SnapshotStateList.toList(),以避免逐元素读取带来的观察开销。
  • 重复读取状态:将重复读取的 Compose State 属性提取到代码块顶部的局部变量中。
  • Setter 中的状态:避免在自定义属性 setter 中读取 Compose State;触发变更副作用时使用原始后备字段,并批量更新多个属性。
  • 稳定性与跳过的权衡(跳过并非免费):不要一味追求稳定性,也不要将 @Immutable、@Stable 或 ImmutableList 视为默认解决方案。跳过 composable 并非没有成本:每个参数都会像被记住一样存储在 slot table 中,并且每次重组都必须比较所有输入。强跳过模式 已会在实例相等(===)时跳过带有不稳定参数(包括标准 kotlin.collections.List)的 composable。添加注解或使用 ImmutableList 会强制进行结构相等比较(.equals());对于大型集合或复杂类,这可能比重组本身开销更大。
  • 绝不要给集合模型添加 @Stable、@Immutable 或 ImmutableList:编写或优化代码时,绝不要给包含集合(List、Set、Map)的模型添加 @Immutable 或 @Stable,也绝不要建议将 kotlin.collections.List<T> 替换为 ImmutableList<T>(或在 stability_config.txt 中添加 ImmutableList)。在强跳过模式下,每次重组都对集合执行 O(N) 的 .equals() 比较,比 O(1) 的实例相等比较(===)开销更大。
  • 移除现有 @Stable / @Immutable 注解的适用范围(仅限大型惰性数据集):
    • 何时移除:仅当同时满足以下两个条件时,才建议从数据模型中移除现有的 @Stable 或 @Immutable 注解:(1) 该模型用作惰性布局(LazyColumn、LazyRow、LazyGrid、Pager、ScalingLazyColumn)中的输入数据源或条目模型,且 (2) 它包含大型数据集或深层嵌套集合层级(例如信息流、大量标签/条目,或多层嵌套列表,如包含 List<Paragraph> 和 List<Markup> 的 Post),其中递归的 O(N) .equals() 检查会降低滚动和重组性能。
    • 何时保持不变:对于代码库中其他所有现有的 @Stable、@Immutable 或 ImmutableList 用法——例如非惰性 UI 模型、独立组件,或集合规模较小/有界的模型(例如带有少量徽章、缩略图或元数据行的卡片),其中数据集较小或规模未知——保留现有注解,不要标记。绝不要不加判断地在整个代码库中搜索并标记所有 @Stable/@Immutable 类;必须先确认它们是否用于包含大型数据集的惰性布局。
  • 强跳过模式下何时添加 @Stable / @Immutable:Compose 模块中属性为 val,且只包含基本类型或 String 的扁平 data class 已被编译器推断为稳定,并会自动使用 .equals()——不要给它们添加冗余的 @Immutable 注解。仅当编译器无法推断稳定性(例如来自非 Compose 模块的类或接口),且数据源(例如 Room 或 MVI reducer)会在每次发射时为未发生变化的条目分配新的对象实例时,才添加 @Stable 或 @Immutable;前提是该类规模较小、结构扁平、不包含集合(List、Set、Map),并且其 .equals() 检查为简单的 O(1)。
  • 让 Composable 更轻量,而不是强行跳过:不要过度关注重组次数。在一次正在进行的重组过程中跳过一个 composable,节省的时间很少,因为一旦开始调度 composition,调度和启动重组过程才是主要开销。重点应放在降低 composable 的执行成本、将状态读取延迟到 Layout/Draw 阶段,并通过基准测试进行验证,而不是想当然地认为跳过能提升性能。

2. 布局和测量优化(references/layouts.md)

必须:在指出或修复此类别中的任何问题之前,你必须阅读 references/layouts.md。

  • 坐标与放置:避免使用 onGloballyPositioned;使用 Modifier.onVisibilityChanged 跟踪视口,使用 Modifier.onPlaced 或 Modifier.onLayoutRectChanged 获取局部边界,并在 Modifier.Node 事件处理程序中按需查询 requireLayoutCoordinates()。
  • 通过自定义 Layout 锚定浮动内容:将浮动标签或工具提示附加到组件时,请使用自定义 Layout 测量并将两个子项相对放置。切勿将 onGloballyPositioned 坐标跟踪到 MutableState 中,再通过 offset 定位浮动内容;这会导致 1 帧延迟,并触发第二次组合过程。
  • 偏移的阶段延迟:将动态偏移包装在 lambda 中(Modifier.offset { IntOffset(...) }),以跳过组合阶段,直接进入布局阶段。
  • 树深度与装饰性包装器:避免冗余的包装 Box 和装饰性 Canvas 节点;直接附加修饰符,或使用 Modifier.drawBehind。
  • 子组合开销:避免在热路径和惰性列表项中使用 BoxWithConstraints / SubcomposeLayout;优先使用标准修饰符、Modifier.layout 或 LayoutModifierNode。
  • 固有测量:谨慎使用固有测量,并避免深度嵌套的固有测量链。

3. 惰性布局优化(references/lazy-layouts.md)

强制要求:在指出或修复此类别中的任何问题之前,你 必须阅读 references/lazy-layouts.md。

  • 惰性布局范围限制:关于 key 和 contentType 的指令仅严格适用于惰性容器(LazyColumn、LazyRow、LazyGrid)。绝不要指出在集合上迭代的标准非惰性容器(Column、Row、Box)存在问题。
  • 必须设置 contentType 和基本类型的 key:惰性布局(LazyColumn、LazyRow、LazyGrid)中的所有项都必须指定基本类型(Int、Long、String)的 key 和 contentType。使用 items 或 itemsIndexed 时,绝不要省略 contentType:contentType = { _, item -> item.type }。省略 contentType 会导致 Compose 在滚动期间无法复用组合槽。
  • 项内索引查找:将 list.indexOf(item) 替换为 itemsIndexed,以消除布局阶段的 O(N²) 查找。
  • 可观察的选择和状态查询:将选择跟踪从 mutableStateListOf 改为 MutableState 中的不可变 Set(mutableStateOf(emptySet<String>()))、mutableStateSetOf() 或 Map,以实现 O(1) 查找。将 lambda 提供器(isSelected: () -> Boolean)传递给叶级可组合项,避免父级 items 作用域触发重组。
  • 在惰性项中传递 CompositionLocals:避免在单个惰性项可组合项中查询 CompositionLocal(LocalContext.current、LocalDensity.current、LocalSharedTransitionScope.current、作用域等);应在父容器或屏幕中解析一次,并将解析后的值作为参数向下传递。
  • 延迟惰性项中的共享转场:将惰性项上的 Modifier.sharedBounds 或 Modifier.sharedElement 放在可见性指示器(如 Modifier.onVisibilityChanged 或视口可见性检查)之后,以免屏幕外或预取的项实例化转场作用域。
  • 项数的推导:直接读取集合的 .size,不要将列表计数包装在 derivedStateOf 中。
  • 小型且有界的列表:对于小型或有界集合(例如 1 到 10 个项),使用标准 Row 或 Column 配合 for 或 forEach 循环,消除惰性虚拟化、测量和预取开销。
  • 提升共享 Flow:在惰性布局之前的父容器中收集一次共享 Flow;绝不要对每个可见项调用 collectAsStateWithLifecycle()。
  • 避免在预取项中执行副作用:绝不要在惰性项内通过 LaunchedEffect(Unit) 触发分析;应使用带有停留时长阈值的 Modifier.onVisibilityChanged。

4. 动画和共享转场(references/animation.md)

强制要求:在指出或修复此类别中的任何问题之前,你 必须阅读 references/animation.md。

  • 动画属性的阶段延迟:将快速变化的动画状态读取(alpha、scale、rotation、color)包装在 Modifier.graphicsLayer { ... } 中,以绕过组合阶段,直接在绘制阶段求值。
  • 偏移和尺寸的阶段延迟:将动画偏移量包装在 Modifier.offset { IntOffset(...) } 中,并使用 Modifier.layout { ... } 或 Modifier.animateBounds 为约束添加动画,以避免尺寸转场期间发生重组。
  • 在快速滚动列表中延迟共享转场:仅在可见时通过 Modifier.onVisibilityChanged 附加 Modifier.sharedBounds 或 Modifier.sharedElement,避免为被丢弃的预取项实例化修饰符和观察对象所产生的开销。
  • Lambda 提供器:向子可组合项传递 lambda 提供器(() -> Float),而不是原始动画值,以便将状态读取延迟到绘制或布局阶段。
  • 在节点中持久化 Animatable:将 Animatable 存储为 Modifier.Node 的持久成员属性,并在 onAttach() 中使用 coroutineScope.launch 启动。

5. 副作用、生命周期与线程处理(effects-and-threading.md)

强制要求:在标记或修复此类别中的任何问题之前,必须阅读 references/effects-and-threading.md。

  • 禁止在主体中直接产生副作用:组合期间,切勿在 Composable 主体中直接执行副作用;请将其包装在 SideEffect、LaunchedEffect 或用户事件回调中。
  • 禁止使用非挂起的 LaunchedEffect 和空的 DisposableEffect:切勿使用 LaunchedEffect 执行同步操作(例如记录视图日志或更新对象)。切勿将带有空 onDispose {} 块的 DisposableEffect 作为变通方法。请改用 SideEffect(在 Compose 1.12+ 中,SideEffect(keys) { ... } 直接支持键;在 Compose < 1.12 中,SideEffect 不接受参数,因此请检查项目的 Compose 版本,并在 SideEffect { ... } 中使用 remember 的持有对象跟踪键的变化)。
  • 缓存主体中的分配:将开销较大的对象分配(例如日期格式化器或复杂路径)和耗时的计算包装在 remember 中。
  • 合并相关的 LaunchedEffect:将多个 LaunchedEffect(Unit) 调用合并为一个 LaunchedEffect,并在其中嵌套 launch { ... } 块,以减少 effect 节点分配和协程启动器开销。
  • 副作用的执行顺序与清理开销:了解应用组合变更时副作用的执行顺序(先运行 DisposableEffect,再运行 SideEffect;而 LaunchedEffect 会在组合的 CoroutineContext 上启动一个协程,该协程以异步方式分派,且不保证在 Draw 之后才执行)。请注意,取消 LaunchedEffect 会产生协程取消和清理开销;在动画或滚动期间避免频繁切换 LaunchedEffect 的键。
  • 将任务移出主线程:使用 produceState 将阻塞式 I/O、繁重的文件解析和数据库操作移至 withContext(Dispatchers.IO)。
  • 延迟布局中禁止在单个条目上使用曝光效果:切勿在延迟布局的条目中使用 LaunchedEffect 或 DisposableEffect 记录曝光日志。延迟布局会在条目进入视口前预先组合它们,导致曝光记录过早或错误。请改用带有停留时长阈值的 Modifier.onVisibilityChanged。
  • 以生命周期感知的方式收集 Flow:对于会主动产生后台工作流的 Android UI Flow 收集,请使用 collectAsStateWithLifecycle();对于轻量级的内存内 StateFlow 或非 Android 作用域,请使用 collectAsState()。
  • 提升接收器与监听器的作用域:使用 DisposableEffect 在屏幕级别注册一次系统监听器和 BroadcastReceiver,而不是在延迟列表条目中注册。
  • 为可选副作用添加保护:在启动副作用前使用 if (callback != null) 进行保护,以避免分配空协程和 effect 节点。
  • 防止捕获过时值:如果 LaunchedEffect 中的长时间运行或循环协程引用了回调或参数,请使用 rememberUpdatedState。
  • 替换旧式计时器:使用 LaunchedEffect 和 delay() 替换 Handler.postDelayed 或 TimerTask。

6. 图形与自定义绘制(references/graphics-performance.md)

强制要求:在标记或修复此类别中的任何问题之前, 必须阅读 references/graphics-performance.md。

  • 缓存绘制分配:使用 Modifier.drawWithCache 替换装饰性 Canvas 节点,以缓存 Path、Paint 和 Brush 分配。
  • 优先使用 AsyncImage,而非 Image:使用 Coil 的 AsyncImage 加载互联网图片,并使用 painterResource 加载可绘制资源。Image 未针对图片缓存和加载进行优化,可能会导致性能问题。
  • 限定快速变化的统一变量作用域:在组合中使用 remember 记住 RuntimeShader 和 ShaderBrush,在 drawWithCache 中更新尺寸统一变量,并在 onDrawBehind 中更新逐帧统一变量。
  • 复用图形路径对象:在热点绘制循环中,优先使用 Path.rewind() 而不是 reset(),以保留已分配的缓冲区,避免重新分配。
  • 值类和原始类型集合:避免使用泛型元组(Pair、Triple)和泛型集合(HashSet<Long>);改用 androidx.collection.MutableLongSet 和 androidx.compose.ui.util.packInts。

7. 自定义修饰符:Modifier.Node(自定义修饰符指南)

  • 迁移到 Modifier.Node:将旧版 Modifier.composed 迁移到 ModifierNodeElement + Modifier.Node(委托给子节点时,例如 SuspendingPointerInputModifierNode,使用 DelegatingNode),并采用能力 接口(DrawModifierNode、LayoutModifierNode、 SemanticsModifierNode、使用 currentValueOf(...) 的 CompositionLocalConsumerModifierNode),以消除每个节点的子组合开销,并 支持节点复用(参见使用 Modifier.Node 创建自定义修饰符):

    // Modifier factory
    fun Modifier.circle(color: Color) = this then CircleElement(color)
    
    // ModifierNodeElement
    private data class CircleElement(
        val color: Color
    ) : ModifierNodeElement<CircleNode>() {
        override fun create() = CircleNode(color)
    
        override fun update(node: CircleNode) {
            node.color = color
        }
    }
    
    // Modifier.Node
    private class CircleNode(
        var color: Color
    ) : DrawModifierNode, Modifier.Node() {
        override fun ContentDrawScope.draw() {
            drawCircle(color)
        }
    }
    

  • 按需获取坐标:在 Modifier.Node 的指针或绘制处理程序中按需调用 requireLayoutCoordinates(),而不是实现 GlobalPositionAwareModifierNode.onGloballyPositioned。

重要:如需了解如何将常见修饰符迁移到 Modifier.Node 的更多示例,必须阅读使用 Modifier.Node 创建自定义修饰符。

评论

支持 Markdown