compose-stability-diagnostics

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose stability diagnostics

Compose稳定性诊断

Core principle

核心原则

Compose performance problems from parameters are about whether inputs compare cheaply and predictably across recompositions. With Kotlin 2.0.20+ strong skipping is enabled by default, so unstable parameters no longer automatically make restartable composables non-skippable. That does not make stability irrelevant: unstable parameters are compared by instance identity (
===
), stable parameters by equality (
equals
), and churny instances can still defeat skipping.
First identify the compiler mode you are on, then read reports in that context.
Compose中由参数引发的性能问题,核心在于跨重组时输入是否能低成本且可预测地进行比较。在Kotlin 2.0.20+版本中,强跳过默认启用,因此不稳定参数不再会自动导致可重启的Composable不可跳过。但这并不意味着稳定性无关紧要:不稳定参数通过实例身份(
===
)进行比较,稳定参数通过相等性(
equals
)比较,而频繁变化的实例仍会破坏跳过机制。
首先确定你使用的编译器模式,再结合该上下文解读报告。

When to use this skill

何时使用此技能

  • A composable or screen recomposes more than expected and parameter churn is suspected.
  • A UI-state/model class is passed to composables and contains
    List
    ,
    Set
    ,
    Map
    , ranges, Java time/money types, or third-party types.
  • composables.txt
    /
    classes.txt
    shows unstable parameters or non-skippable composables.
  • A project uses Kotlin < 2.0.20, disables strong skipping, or has old Compose compiler report guidance.
  • Composable或界面的重组次数超出预期,且怀疑是参数频繁变化导致。
  • UI状态/模型类被传递给Composable,且包含
    List
    Set
    Map
    、范围、Java时间/货币类型或第三方类型。
  • composables.txt
    /
    classes.txt
    显示存在不稳定参数或不可跳过的Composable。
  • 项目使用Kotlin < 2.0.20版本、禁用强跳过,或遵循旧版Compose编译器报告指南。

1. Start with strong skipping

1. 从强跳过开始

On Kotlin 2.0.20+, strong skipping is enabled by default. In that mode:
  • Restartable composables are skippable even when parameters are unstable, unless explicitly opted out.
  • Stable parameters compare with
    equals
    .
  • Unstable parameters compare with instance equality (
    ===
    ).
  • Lambdas inside composables are automatically remembered based on captures.
That means the question changes from "is this composable skippable at all?" to "will these parameters compare the way I expect, and are callers creating new unstable instances every frame?"
For older compiler setups or strong skipping disabled, the legacy rule still matters: a restartable composable with unstable parameters may be restartable but not skippable.
在Kotlin 2.0.20+版本中,强跳过默认启用。在此模式下:
  • 即使参数不稳定,可重启的Composable仍可跳过,除非显式选择退出。
  • 稳定参数通过
    equals
    进行比较。
  • 不稳定参数通过实例相等性(
    ===
    )进行比较。
  • Composable内部的Lambda会基于捕获的内容自动被remember。
这意味着问题从“这个Composable是否完全可跳过?”转变为“这些参数是否按预期方式比较,调用方是否在每一帧都创建新的不稳定实例?”
对于旧版编译器配置或禁用强跳过的情况,旧规则仍然适用:带有不稳定参数的可重启Composable可能是可重启的,但不可跳过。

2. Generate compiler reports

2. 生成编译器报告

With Kotlin 2.0+ the Compose Compiler is configured through the Kotlin Gradle plugin:
kotlin
plugins {
    alias(libs.plugins.android.application) // or android.library / jvm
    alias(libs.plugins.kotlin.android)      // or kotlin.multiplatform / kotlin.jvm
    alias(libs.plugins.compose.compiler)
}

if (providers.gradleProperty("composeReports").orNull == "true") {
    composeCompiler {
        reportsDestination = layout.buildDirectory.dir("compose_compiler")
        metricsDestination = layout.buildDirectory.dir("compose_compiler")
    }
}
Then build the variant whose compiler configuration you care about, for example:
bash
./gradlew :app:assembleRelease -PcomposeReports=true
Use release/non-debuggable builds for runtime profiling. Compiler reports are build-time outputs, so the important thing is matching the variant and compiler flags you ship.
Key files:
FileWhat it tells you
<module>-classes.txt
Stability of classes and properties
<module>-composables.txt
Restartable/skippable status and parameter stability
<module>-composables.csv
Same data in sortable form
<module>-module.json
Aggregate metrics
在Kotlin 2.0+版本中,Compose编译器通过Kotlin Gradle插件进行配置:
kotlin
plugins {
    alias(libs.plugins.android.application) // 或 android.library / jvm
    alias(libs.plugins.kotlin.android)      // 或 kotlin.multiplatform / kotlin.jvm
    alias(libs.plugins.compose.compiler)
}

if (providers.gradleProperty("composeReports").orNull == "true") {
    composeCompiler {
        reportsDestination = layout.buildDirectory.dir("compose_compiler")
        metricsDestination = layout.buildDirectory.dir("compose_compiler")
    }
}
然后构建你关注其编译器配置的变体,例如:
bash
./gradlew :app:assembleRelease -PcomposeReports=true
使用发布版/非可调试版本进行运行时分析。编译器报告是构建时输出,因此重要的是匹配你发布时使用的变体和编译器标志。
关键文件:
文件说明
<module>-classes.txt
类和属性的稳定性
<module>-composables.txt
可重启/可跳过状态及参数稳定性
<module>-composables.csv
相同数据的可排序格式
<module>-module.json
聚合指标

3. Fix stability where semantics need it

3. 在语义需要的地方修复稳定性

Pick the lightest fix that makes the type's immutability or equality semantics true.
选择最轻量的修复方案,使类型的不可变性或相等性语义成立。

Immutable collections

不可变集合

kotlin.collections.List
is an interface; Compose cannot know the runtime implementation is immutable. Prefer
kotlinx.collections.immutable
at UI-state boundaries:
kotlin
// Before: unstable collection interfaces
data class UiState(val items: List<Item>, val tags: Set<String>)

// After: immutable collection contracts
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet

data class UiState(val items: ImmutableList<Item>, val tags: ImmutableSet<String>)
Producers convert once at the boundary with
.toImmutableList()
/
.toImmutableSet()
.
kotlin.collections.List
是一个接口;Compose无法知道其运行时实现是不可变的。在UI状态边界处优先使用
kotlinx.collections.immutable
kotlin
// 之前:不稳定的集合接口
data class UiState(val items: List<Item>, val tags: Set<String>)

// 之后:不可变集合契约
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet

data class UiState(val items: ImmutableList<Item>, val tags: ImmutableSet<String>)
生产者在边界处通过
.toImmutableList()
/
.toImmutableSet()
进行一次转换。

@Immutable
/
@Stable

@Immutable
/
@Stable

  • Use
    @Immutable
    when every property is effectively immutable and equality describes all observable state.
  • Use
    @Stable
    for types whose mutable state is observable by Compose, typically via
    MutableState
    .
Do not annotate to silence a report. A false stability promise can produce stale UI.
  • 当每个属性实际上都是不可变的,且相等性描述了所有可观察状态时,使用
    @Immutable
  • 对于其可变状态可被Compose观察到的类型(通常通过
    MutableState
    ),使用
    @Stable
不要为了消除报告而添加注解。错误的稳定性承诺会导致UI过时。

Third-party immutable types

第三方不可变类型

For types you cannot annotate, use
stabilityConfigurationFiles
:
kotlin
composeCompiler {
    stabilityConfigurationFiles.add(
        rootProject.layout.projectDirectory.file("compose_stability.conf"),
    )
}
text
java.math.BigDecimal
java.math.BigInteger
java.time.*
kotlinx.datetime.*
Only list types you are willing to promise are immutable. Do not list mutable types such as
java.util.Date
.
对于无法添加注解的类型,使用
stabilityConfigurationFiles
kotlin
composeCompiler {
    stabilityConfigurationFiles.add(
        rootProject.layout.projectDirectory.file("compose_stability.conf"),
    )
}
text
java.math.BigDecimal
java.math.BigInteger
java.time.*
kotlinx.datetime.*
仅列出你能保证是不可变的类型。不要列出可变类型,如
java.util.Date

Quick reference

快速参考

SymptomDiagnosisFix
Kotlin 2.0.20+ but old docs say unstable means non-skippableStrong skipping changed the defaultCheck comparison semantics and instance churn instead
unstable val items: List<Item>
Interface collectionUse
ImmutableList<Item>
or another true immutable wrapper
unstable val price: BigDecimal
External immutable typeAdd to stability config
@Immutable
on a type with mutable internals
False promiseFix the model or remove the annotation
Composable skips poorly despite strong skippingNew unstable instance each recompositionRemember, hoist, or make the type stable/equality-based
Reports not generatedCompose compiler plugin missing or flag not setApply
org.jetbrains.kotlin.plugin.compose
and enable destinations
症状诊断修复方案
使用Kotlin 2.0.20+但旧文档称不稳定意味着不可跳过强跳过改变了默认行为改为检查比较语义和实例频繁变化情况
unstable val items: List<Item>
接口集合使用
ImmutableList<Item>
或其他真正的不可变包装器
unstable val price: BigDecimal
外部不可变类型添加到稳定性配置中
对内部可变的类型使用
@Immutable
错误的承诺修复模型或移除注解
尽管启用了强跳过,Composable的跳过效果仍很差每次重组都创建新的不稳定实例使用remember、提升状态,或使类型稳定/基于相等性
未生成报告缺少Compose编译器插件或未设置标志应用
org.jetbrains.kotlin.plugin.compose
并启用输出目录

When NOT to apply

何时不适用

  • The issue is a fast-changing
    State
    read in composition, such as scroll or animation. Use
    compose-state-deferred-reads
    .
  • The recomposition count matches real data changes.
  • The bug is wrong data or stale state, not excess work.
  • The code is test-only and readability is more important than report cleanliness.
  • 问题是组合中快速变化的
    State
    读取,例如滚动或动画。请使用
    compose-state-deferred-reads
  • 重组次数与实际数据变化匹配。
  • 错误是数据错误或过时状态,而非额外的工作。
  • 代码仅用于测试,可读性比报告整洁度更重要。

Related

相关技能

  • compose-state-deferred-reads
    - frame-rate state should often be read in layout/draw rather than composition.
  • compose-recomposition-performance
    - entry point when you are not sure which recomposition axis is involved.
  • compose-state-deferred-reads
    - 帧率相关状态通常应在布局/绘制阶段读取,而非组合阶段。
  • compose-recomposition-performance
    - 当你不确定涉及哪个重组维度时的入口点。