jetpack-compose-performance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Foundational prerequisites

基础前提

Before micro-optimizing composables, ensure standard production flags are in place:
  1. Release Build: Never profile or benchmark debuggable builds; ART runtime verification severely degrades performance.
  2. R8 Minification: Keep R8 enabled to optimize bytecode and coroutine dispatch.
  3. Baseline Profiles: Generate baseline profiles for critical user journeys (cold start, key scrolling feeds) so the ART VM pre-compiles and preloads classes ahead of time.
在对 composable 进行微优化之前,确保标准生产环境构建标志已正确配置:
  1. Release Build:切勿对可调试构建进行性能分析或基准测试;ART 运行时验证会严重降低性能。
  2. R8 Minification:保持启用 R8,以优化字节码和协程调度。
  3. Baseline Profiles:为关键用户旅程(冷启动、关键滚动信息流)生成 baseline profile,使 ART VM 能够提前预编译和预加载类。

Optimization audit workflow

优化审计工作流

  1. Exhaustive Scan : Scan the target code against all 7 core categories in the following sections. Evaluate every
    @Composable
    function, modifier chain, and lazy-layout data model against all checklist rules rather than stopping after the first issue found.
  2. Mandatory Reference Read Before Flagging or Editing : Whenever a file matches a rule in Categories 1--7, you MUST open and read the corresponding
    references/*.md
    file for the approved fix pattern and full scope guards before reporting a finding or editing code. Never rely on the summary bullets alone to write fixes.
  1. 全面扫描:依据后续各节中的全部 7 个核心类别扫描目标代码。应根据所有检查清单规则评估每一个
    @Composable
    函数、modifier 链和 lazy layout 数据模型,而不是在发现第一个问题后便停止。
  2. 标记问题或编辑前强制阅读参考文档:每当文件符合类别 1--7 中某项规则时,在报告问题或编辑代码之前,必须打开并阅读相应的
    references/*.md
    文件,以了解认可的修复模式和完整的适用范围约束。编写修复方案时,绝不能仅依赖摘要要点。

1. State, snapshots, and stability (references/state-and-stability.md)

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

MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/state-and-stability.md.
  • Phase Deferral (Lambdas) : Wrap frequently changing state reads in lambdas (such as
    Modifier.offset
    or
    Modifier.graphicsLayer
    ) so reads occur in Layout or Draw phase rather than Composition.
  • Prohibit Backwards Writes (Verify Read-Before-Write Order) : Never modify state in a later phase or after it has already been read in the same composition pass (
    Read -> Write
    , or
    Composition Read -> Layout or Draw Write
    ). Prevent backwards writes across phases to avoid first-frame popping, jank, and infinite recomposition loops (Backwards Write). Do not flag a state assignment at the start of a composable (
    Write -> Read
    or
    Composition Write -> Draw Read
    ) as a backwards write---a backwards write strictly requires a read of that state to precede the write.
  • Avoid Redundant State Wrapping in Composition (
    Write -> Read
    )
    : Do not wrap synchronous values in
    remember { mutableStateOf(...) }
    only to overwrite them immediately at the top of composition before reading them (for example,
    var path by remember { mutableStateOf(Path()) }; path = viewModel.getPath()
    ). While not a backwards write, this adds unnecessary slot table and
    MutableState
    snapshot overhead (and risks becoming a backwards write if a read is later added preceding the assignment); use a plain local
    val
    or read the value directly inside the target phase (such as
    Modifier.drawWithCache
    ).
  • Explicit
    remember
    Keys & Slot Overhead
    : Always pass all input variables and composable parameters referenced inside
    remember
    as calculation keys. Remember that
    remember
    stores
    keys + 1
    objects in the slot table and checks key equality on every recomposition; only
    remember
    when allocations are heavy or identity must be preserved. Do not pass snapshot state values/properties (e.g.
    state.value
    ) as keys when wrapping
    derivedStateOf
    (passing the
    State
    instance itself is fine if the instance reference can change).
  • computedStateOf
    vs
    derivedStateOf
    (Compose 1.13+)
    : On Compose 1.13+, use
    computedStateOf
    for simple condition checks and filtered state from fast-changing inputs (such as
    scrollState.value > 0
    ), which provides significantly faster invalidation than
    derivedStateOf
    (on Compose
    < 1.13
    , use
    remember { derivedStateOf { ... } }
    ). Reserve
    derivedStateOf
    for heavy calculations that benefit from multi-read caching, or trivial calculations read frequently enough per composition (roughly
    10 / numberOfStatesReadPerCalculation
    times without invalidating between reads) to make up for the invalidation cost.
  • Synchronous State Derivation : Audit for asynchronous
    produceState
    +
    snapshotFlow
    blocks observing Compose states (such as scroll positions or collection sizes); replace them with synchronous
    remember { derivedStateOf { ... } }
    (or
    computedStateOf
    on Compose 1.13+).
  • Trivial Primitives : Prefer plain custom Kotlin getters (
    get()
    ) over
    derivedStateOf
    or
    computedStateOf
    for simple boolean expressions and property lookups.
  • Collection State Tracking : Back whole-list updates with standard
    mutableListOf()
    and
    mutableStateOf(list, neverEqualPolicy())
    with load-bearing assignments instead of
    mutableStateListOf()
    .
  • Snapshot Collections : Use
    SnapshotStateList.toList()
    for repeated queries or iterations to avoid per-element read observation overhead.
  • Repeated State Reads : Extract repeated Compose
    State
    property reads into a local variable at the top of the block.
  • State in Setters : Avoid reading Compose
    State
    inside custom property setters; use a raw backing field when triggering change side-effects and batch multi-property updates.
  • Stability & Skipping Trade-Offs (Skipping Isn't Free) : Do not fixate on stability or treat
    @Immutable
    ,
    @Stable
    , or
    ImmutableList
    as default solutions. Skipping a composable is not free: each parameter is stored in the slot table as if remembered, and all inputs must be compared on recomposition. Strong Skipping Mode already skips composables with unstable parameters (including standard
    kotlin.collections.List
    ) when instance equality (
    ===
    ) matches. Annotating models or using
    ImmutableList
    forces structural equality (
    .equals()
    ), which can be significantly more expensive than recomposition itself for large collections or complex classes.
  • Never Add
    @Stable
    ,
    @Immutable
    , or
    ImmutableList
    to Collection Models
    : When writing or optimizing code, never add
    @Immutable
    or
    @Stable
    to models containing collections (
    List
    ,
    Set
    ,
    Map
    ), and never recommend replacing
    kotlin.collections.List<T>
    with
    ImmutableList<T>
    (or adding
    ImmutableList
    to
    stability_config.txt
    ). Calling
    O(N)
    .equals()
    on collections on every recomposition pass is more expensive than
    O(1)
    instance equality (
    ===
    ) under Strong Skipping Mode.
  • Scope Guard for Removing Existing
    @Stable
    /
    @Immutable
    Annotations (Large Lazy Datasets Only)
    :
    • When to Remove : Only recommend removing existing
      @Stable
      or
      @Immutable
      annotations from data models when both conditions are met: (1) the model is used within a lazy layout (
      LazyColumn
      ,
      LazyRow
      ,
      LazyGrid
      ,
      Pager
      ,
      ScalingLazyColumn
      ) as the input feed or item model, and (2) it contains a large dataset or deep nested collection hierarchy (e.g., feed streams, large lists of tags/items, or multi-level nested lists like
      Post
      with
      List<Paragraph>
      and
      List<Markup>
      ), where recursive
      O(N)
      .equals()
      checks degrade scrolling and recomposition performance.
    • When to Leave Untouched : For all other existing
      @Stable
      ,
      @Immutable
      , or
      ImmutableList
      usages in a codebase---such as non-lazy UI models, standalone components, or models with small/bounded collections (e.g., a card with a few badges, thumbnails, or metadata rows) where dataset size is small or unknown---leave the existing annotations untouched and do not flag them . Never blindly grep and flag all
      @Stable
      /
      @Immutable
      classes across a codebase without verifying they feed a lazy layout with a large dataset.
  • When to Add
    @Stable
    /
    @Immutable
    Under Strong Skipping Mode
    : Flat
    data class
    es with
    val
    primitive/
    String
    properties in Compose modules are already inferred as stable by the compiler and use
    .equals()
    automatically---do not add redundant
    @Immutable
    annotations to them. Only add
    @Stable
    or
    @Immutable
    when the compiler cannot infer stability (e.g. classes from non-Compose modules or interfaces) and a data source (such as Room or MVI reducers) allocates new object instances for unchanged items on every emission, provided the class is small, flat, has no collections (
    List
    ,
    Set
    ,
    Map
    ), and its
    .equals()
    check is trivial
    O(1)
    .
  • Make Composables Cheaper Instead of Forcing Skipping: Do not overindex on recomposition counts. Skipping one composable during an active recomposition pass saves little time once composition is scheduled (leaving the house is the bulk of the cost). Focus on making composables cheap to execute, deferring state reads to Layout/Draw, and verifying with benchmarks rather than assuming skipping improves performance.
强制要求:在本类别中标记或修复任何问题之前,必须阅读 references/state-and-stability.md。
  • 阶段延迟(Lambda):将频繁变化的状态读取包装在 lambda 中(如
    Modifier.offset
    或
    Modifier.graphicsLayer
    ),使读取发生在 Layout 或 Draw 阶段,而不是 Composition 阶段。
  • 禁止反向写入(验证读取先于写入的顺序):绝不要在更晚的阶段修改状态,或在同一 composition pass 中读取状态后才修改状态(
    Read -> Write
    ,或
    Composition Read -> Layout or Draw Write
    )。防止跨阶段反向写入,以避免首帧跳变、卡顿和无限 recomposition 循环(Backwards Write)。不要将 composable 开头的状态赋值标记为反向写入——反向写入严格要求先读取该状态,再执行写入。
  • 避免在 Composition 中重复包装状态(
    Write -> Read
    )
    :不要仅仅为了在 composition 顶部读取前立即覆盖同步值,就将其包装在
    remember { mutableStateOf(...) }
    中(例如
    var path by remember { mutableStateOf(Path()) }; path = viewModel.getPath()
    )。这虽然不属于反向写入,但会增加不必要的 slot table 和
    MutableState
    snapshot 开销(如果在赋值前新增读取,还可能变成反向写入);应改用普通局部
    val
    ,或直接在目标阶段(如
    Modifier.drawWithCache
    )内读取该值。
  • 显式
    remember
    键与 Slot 开销
    :始终将
    remember
    内部引用的所有输入变量和 composable 参数作为计算键传入。需注意,
    remember
    会在 slot table 中存储
    keys + 1
    个对象,并在每次 recomposition 时检查键的相等性;仅当对象分配成本较高或必须保持标识时才使用
    remember
    。使用
    derivedStateOf
    包装时,不要将 snapshot 状态值或属性(如
    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()
    。
  • Snapshot 集合:对于重复查询或遍历,使用
    SnapshotStateList.toList()
    ,以避免按元素读取所产生的观察开销。
  • 重复状态读取:将重复的 Compose
    State
    属性读取提取到块顶部的局部变量中。
  • Setter 中的状态:避免在自定义属性 setter 中读取 Compose
    State
    ;触发变更副作用时应使用原始 backing field,并批量执行多属性更新。
  • 稳定性与跳过之间的权衡(跳过并非没有成本):不要过度关注稳定性,也不要将
    @Immutable
    、
    @Stable
    或
    ImmutableList
    视为默认解决方案。跳过 composable 并非没有成本:每个参数都会像被
    remember
    一样存储在 slot table 中,并且在每次 recomposition 时必须比较所有输入。在实例相等性(
    ===
    )匹配时,Strong Skipping Mode 已经能够跳过具有不稳定参数(包括标准
    kotlin.collections.List
    )的 composable。给模型添加注解或使用
    ImmutableList
    会强制采用结构相等性(
    .equals()
    );对于大型集合或复杂类,这可能比 recomposition 本身昂贵得多。
  • 绝不要给集合模型添加
    @Stable
    、
    @Immutable
    或
    ImmutableList
    :编写或优化代码时,绝不要给包含集合(
    List
    、
    Set
    、
    Map
    )的模型添加
    @Immutable
    或
    @Stable
    ,也不要建议将
    kotlin.collections.List<T>
    替换为
    ImmutableList<T>
    (或将其添加到
    stability_config.txt
    )。在 Strong Skipping Mode 下,每次 recomposition 都对集合执行
    O(N)
    的
    .equals()
    ,成本会高于
    O(1)
    的实例相等性检查(
    ===
    )。
  • 移除现有
    @Stable
    /
    @Immutable
    注解的适用范围约束(仅限大型 Lazy 数据集)
    :
    • 何时移除:仅当同时满足以下两个条件时,才建议从数据模型中移除现有的
      @Stable
      或
      @Immutable
      注解:(1) 该模型在 lazy layout(
      LazyColumn
      、
      LazyRow
      、
      LazyGrid
      、
      Pager
      、
      ScalingLazyColumn
      )中作为输入数据源或 item 模型使用;且 (2) 该模型包含大型数据集或深层嵌套集合层级(例如信息流、大型标签或 item 列表,或类似
      Post
      包含
      List<Paragraph>
      和
      List<Markup>
      的多层嵌套列表),其中递归的
      O(N)
      .equals()
      检查会降低滚动和 recomposition 性能。
    • 何时保持不变:对于代码库中其他所有现有的
      @Stable
      、
      @Immutable
      或
      ImmutableList
      用法——例如非 lazy UI 模型、独立组件,或包含小型、有界集合的模型(例如包含少量徽章、缩略图或元数据行的卡片)——如果数据集规模较小或未知,应保持现有注解不变且不要将其标记为问题。绝不能在整个代码库中盲目搜索并标记所有
      @Stable
      /
      @Immutable
      类,而未验证其是否为大 lazy layout 提供大型数据集。
  • 在 Strong Skipping Mode 下何时添加
    @Stable
    /
    @Immutable
    :Compose 模块中仅包含
    val
    基本类型或
    String
    属性的扁平
    data class
    已由编译器推断为稳定类型,并自动使用
    .equals()
    ——不要为其添加冗余的
    @Immutable
    注解。仅当编译器无法推断稳定性时(例如来自非 Compose 模块的类或接口),且数据源(如 Room 或 MVI reducer)在每次发射时都会为未变化的 item 分配新对象实例,才应添加
    @Stable
    或
    @Immutable
    ;同时该类必须体积小、结构扁平、不包含集合(
    List
    、
    Set
    、
    Map
    ),且其
    .equals()
    检查必须是简单的
    O(1)
    操作。
  • 让 Composable 更轻量,而非强制跳过:不要过度关注 recomposition 次数。一旦 composition 被调度,跳过其中一个 composable 所节省的时间非常有限(真正的大头成本在 composition 本身)。应重点让 composable 的执行成本更低、将状态读取延迟到 Layout/Draw 阶段,并通过 benchmark 验证效果,而不是假定跳过 composable 一定能提升性能。

2. Layouts and measurement optimization (references/layouts.md)

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

MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/layouts.md.
  • Coordinates & Placement : Avoid
    onGloballyPositioned
    ; use
    Modifier.onVisibilityChanged
    for viewport tracking,
    Modifier.onPlaced
    or
    Modifier.onLayoutRectChanged
    for local bounds, and query
    requireLayoutCoordinates()
    on demand inside
    Modifier.Node
    event handlers.
  • Anchor Floating Content via Custom
    Layout
    : When attaching floating labels or tooltips to a component, use a custom
    Layout
    measuring and placing both children relative to each other. Never track
    onGloballyPositioned
    coordinates in
    MutableState
    to position floating content with
    offset
    , which causes a 1-frame lag and schedules a second composition pass.
  • Phase Deferral for Offsets : Wrap dynamic offsets in lambdas (
    Modifier.offset { IntOffset(...) }
    ) to skip Composition straight to Layout.
  • Tree Depth & Decorative Wrappers : Avoid redundant wrapper
    Box
    es and decorative
    Canvas
    nodes; attach modifiers directly or use
    Modifier.drawBehind
    .
  • Subcomposition Overhead : Avoid
    BoxWithConstraints
    /
    SubcomposeLayout
    inside hot paths and lazy items; prefer standard modifiers,
    Modifier.layout
    , or
    LayoutModifierNode
    .
  • Intrinsic Measurements: Use intrinsic measurements judiciously and avoid deeply nested intrinsic chains.
强制要求:在本类别中标记或修复任何问题之前,必须阅读 references/layouts.md。
  • 坐标与位置:避免使用
    onGloballyPositioned
    ;使用
    Modifier.onVisibilityChanged
    跟踪视口,使用
    Modifier.onPlaced
    或
    Modifier.onLayoutRectChanged
    跟踪局部边界,并在
    Modifier.Node
    事件处理器内按需调用
    requireLayoutCoordinates()
    。
  • 通过自定义
    Layout
    锚定浮动内容
    :将浮动标签或 tooltip 附加到组件时,使用自定义
    Layout
    相对彼此测量和放置两个子节点。绝不要在
    MutableState
    中跟踪
    onGloballyPositioned
    坐标并使用
    offset
    定位浮动内容,否则会导致 1 帧延迟并调度第二次 composition pass。
  • 偏移的阶段延迟:将动态偏移包装在 lambda 中(
    Modifier.offset { IntOffset(...) }
    ),以跳过 Composition 并直接进入 Layout 阶段。
  • 树深度与装饰性包装器:避免冗余的包装
    Box
    和装饰性
    Canvas
    节点;应直接附加 modifier,或使用
    Modifier.drawBehind
    。
  • Subcomposition 开销:避免在热路径和 lazy item 中使用
    BoxWithConstraints
    /
    SubcomposeLayout
    ;优先使用标准 modifier、
    Modifier.layout
    或
    LayoutModifierNode
    。
  • Intrinsic 测量:谨慎使用 intrinsic measurement,并避免深层嵌套的 intrinsic 测量链。

3. Lazy layouts optimization (references/lazy-layouts.md)

3. Lazy layout 优化(references/lazy-layouts.md)

MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/lazy-layouts.md.
  • Lazy Layout Scope Guard : Directives for
    key
    and
    contentType
    apply strictly to Lazy containers (
    LazyColumn
    ,
    LazyRow
    ,
    LazyGrid
    ). Never flag standard non-lazy containers (
    Column
    ,
    Row
    ,
    Box
    ) iterating over collections.
  • Mandatory
    contentType
    and Primitive
    key
    : Always specify both primitive
    key
    s (
    Int
    ,
    Long
    ,
    String
    ) and
    contentType
    s for all items in lazy layouts (
    LazyColumn
    ,
    LazyRow
    ,
    LazyGrid
    ). When using
    items
    or
    itemsIndexed
    , never omit
    contentType
    :
    contentType = { _, item -> item.type }
    . Omitting
    contentType
    prevents Compose from reusing composition slots during scrolling.
  • In-Item Index Lookups : Replace
    list.indexOf(item)
    with
    itemsIndexed
    to eliminate
    O(N²)
    lookups during layout passes.
  • Observable Selection & State Queries : Convert selection tracking from
    mutableStateListOf
    to an immutable
    Set
    in
    MutableState
    (
    mutableStateOf(emptySet<String>())
    ),
    mutableStateSetOf()
    , or Map for
    O(1)
    lookups. Pass lambda providers (
    isSelected: () -> Boolean
    ) to leaf composables to isolate recomposition from parent
    items
    scopes.
  • Propagate CompositionLocals in Lazy Items : Avoid querying
    CompositionLocal
    s (
    LocalContext.current
    ,
    LocalDensity.current
    ,
    LocalSharedTransitionScope.current
    , scopes, etc.) inside individual lazy item composables; resolve them once in the parent container or screen and propagate the resolved values down as parameters.
  • Defer Shared Transitions in Lazy Items : Put
    Modifier.sharedBounds
    or
    Modifier.sharedElement
    on lazy items behind visibility indicators (such as
    Modifier.onVisibilityChanged
    or viewport visibility checks) so offscreen or pre-fetched items do not instantiate transition scopes.
  • Item Count Derivations : Read collection
    .size
    directly instead of wrapping list counts in
    derivedStateOf
    .
  • Small & Bounded Lists : Use standard
    Row
    or
    Column
    with
    for
    or
    forEach
    loops for small or bounded collections (e.g. 1 to 10 items) to eliminate lazy virtualization, measuring, and pre-fetching overhead.
  • Hoist Shared Flows : Collect shared
    Flow
    s once in the parent container preceding lazy layouts; never call
    collectAsStateWithLifecycle()
    per visible item.
  • Avoid Side Effects in Pre-Fetched Items : Never trigger analytics in
    LaunchedEffect(Unit)
    inside lazy items; use
    Modifier.onVisibilityChanged
    with dwell thresholds instead.
强制要求:在本类别中标记或修复任何问题之前,必须阅读 references/lazy-layouts.md。
  • Lazy layout 适用范围约束:有关
    key
    和
    contentType
    的指令严格适用于 lazy 容器(
    LazyColumn
    、
    LazyRow
    、
    LazyGrid
    )。绝不要将遍历集合的标准非 lazy 容器(
    Column
    、
    Row
    、
    Box
    )标记为问题。
  • 必须提供
    contentType
    和基本类型
    key
    :始终为 lazy layout(
    LazyColumn
    、
    LazyRow
    、
    LazyGrid
    )中的所有 item 同时指定基本类型
    key
    (
    Int
    、
    Long
    、
    String
    )和
    contentType
    。使用
    items
    或
    itemsIndexed
    时,绝不能省略
    contentType
    :
    contentType = { _, item -> item.type }
    。省略
    contentType
    会导致 Compose 在滚动期间无法复用 composition slot。
  • Item 内索引查找:将
    list.indexOf(item)
    替换为
    itemsIndexed
    ,以消除布局阶段中的
    O(N²)
    查找。
  • 可观察选择与状态查询:将选择跟踪从
    mutableStateListOf
    转换为
    MutableState
    中的不可变
    Set
    (
    mutableStateOf(emptySet<String>())
    )、
    mutableStateSetOf()
    或
    Map
    ,以实现
    O(1)
    查找。将 lambda provider(
    isSelected: () -> Boolean
    )传递给叶子 composable,以将 recomposition 与父级
    items
    scope 隔离。
  • 在 Lazy item 中传递 CompositionLocals:避免在各个 lazy item composable 内部查询
    CompositionLocal
    (如
    LocalContext.current
    、
    LocalDensity.current
    、
    LocalSharedTransitionScope.current
    、scope 等);应在父容器或 screen 中解析一次,再将解析后的值作为参数向下传递。
  • 延迟 Lazy item 中的共享转场:仅在满足可见性条件时(例如使用
    Modifier.onVisibilityChanged
    或视口可见性检查)才在 lazy item 上附加
    Modifier.sharedBounds
    或
    Modifier.sharedElement
    ,避免屏幕外或预取 item 创建 transition scope。
  • Item 数量派生:直接读取集合的
    .size
    ,不要将列表数量包装在
    derivedStateOf
    中。
  • 小型且有界的列表:对于小型或有界集合(例如 1 到 10 个 item),使用带
    for
    或
    forEach
    循环的标准
    Row
    或
    Column
    ,以消除 lazy 虚拟化、测量和预取开销。
  • 上提共享 Flow:在父容器中、lazy layout 之前仅收集一次共享
    Flow
    ;绝不要对每个可见 item 调用
    collectAsStateWithLifecycle()
    。
  • 避免在预取 item 中触发副作用:绝不要在 lazy item 的
    LaunchedEffect(Unit)
    中触发 analytics;应改用带停留时长阈值的
    Modifier.onVisibilityChanged
    。

4. Animation and shared transitions (references/animation.md)

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

MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/animation.md.
  • Phase Deferral for Animated Properties : Wrap fast-changing animated state reads (alpha, scale, rotation, color) in
    Modifier.graphicsLayer { ... }
    to bypass Composition and evaluate directly in the Draw phase.
  • Phase Deferral for Offsets & Size : Wrap animated offsets in
    Modifier.offset { IntOffset(...) }
    and animate constraints using
    Modifier.layout { ... }
    or
    Modifier.animateBounds
    to bypass recomposition during size transitions.
  • Defer Shared Transitions in Fast-Scrolling Lists : Attach
    Modifier.sharedBounds
    or
    Modifier.sharedElement
    only when visible using
    Modifier.onVisibilityChanged
    to avoid modifier and observation instantiation costs on discarded pre-fetched items.
  • Lambda Providers : Pass lambda providers (
    () -> Float
    ) instead of raw animated values to child composables to keep state reads deferred to Draw or Layout.
  • Persistent
    Animatable
    in Nodes
    : Store
    Animatable
    as a persistent member property on
    Modifier.Node
    and launch in
    onAttach()
    using
    coroutineScope.launch
    .
强制要求:在本类别中标记或修复任何问题之前,必须阅读 references/animation.md。
  • 动画属性的阶段延迟:将快速变化的动画状态读取(alpha、scale、rotation、color)包装在
    Modifier.graphicsLayer { ... }
    中,以绕过 Composition 并直接在 Draw 阶段求值。
  • 偏移与尺寸的阶段延迟:将动画偏移包装在
    Modifier.offset { IntOffset(...) }
    中,并使用
    Modifier.layout { ... }
    或
    Modifier.animateBounds
    动画化约束,以在尺寸变化期间绕过 recomposition。
  • 在快速滚动列表中延迟共享转场:仅在 item 可见时使用
    Modifier.onVisibilityChanged
    附加
    Modifier.sharedBounds
    或
    Modifier.sharedElement
    ,以避免对已丢弃的预取 item 产生 modifier 和观察实例化开销。
  • Lambda Provider:向子 composable 传递 lambda provider(
    () -> Float
    ),而不是原始动画值,以将状态读取延迟到 Draw 或 Layout 阶段。
  • 在 Node 中持久保存
    Animatable
    :将
    Animatable
    作为
    Modifier.Node
    上的持久成员属性保存,并在
    onAttach()
    中使用
    coroutineScope.launch
    启动。

5. Effects, lifecycle, and threading (effects-and-threading.md)

5. Effect、生命周期与线程(effects-and-threading.md)

MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/effects-and-threading.md.
  • Prohibit Direct Side-Effects in Body : Never perform side-effects directly in the Composable body during composition; wrap them in
    SideEffect
    ,
    LaunchedEffect
    , or user event callbacks.
  • Prohibit Non-Suspending
    LaunchedEffect
    & Empty
    DisposableEffect
    : Never use
    LaunchedEffect
    for synchronous operations (e.g. logging views or updating objects). Never use
    DisposableEffect
    with an empty
    onDispose {}
    block as a workaround. Use
    SideEffect
    instead (in Compose 1.12+,
    SideEffect(keys) { ... }
    supports keys directly; on Compose
    < 1.12
    ,
    SideEffect
    takes no arguments, so check the project's Compose version and track key changes with a
    remember
    ed holder inside
    SideEffect { ... }
    ).
  • Cache Allocations in Body : Wrap heavy object allocations (such as date formatters or complex paths) and expensive computations in
    remember
    .
  • Combine Related
    LaunchedEffect
    s
    : Combine multiple
    LaunchedEffect(Unit)
    calls into a single
    LaunchedEffect
    with nested
    launch { ... }
    blocks to reduce effect node allocations and coroutine launcher overhead.
  • Effect Ordering & Teardown Costs : Respect effect execution order (
    DisposableEffect
    >
    SideEffect
    > Layout and Draw >
    LaunchedEffect
    after frame). Note that canceling a
    LaunchedEffect
    incurs coroutine cancellation and teardown overhead; avoid cycling
    LaunchedEffect
    keys frequently during animations or scrolling.
  • Main Thread Offloading : Offload blocking I/O, heavy file parsing, and database operations to
    withContext(Dispatchers.IO)
    using
    produceState
    .
  • No Item-Level Impression Effects in Lazy Layouts : Never use
    LaunchedEffect
    or
    DisposableEffect
    inside lazy items for impression logging. Lazy items are pre-composed before entering the viewport, causing premature or false impressions. Use
    Modifier.onVisibilityChanged
    with dwell thresholds instead.
  • Lifecycle-Aware Flow Collection : Use
    collectAsStateWithLifecycle()
    for UI Flow collections that actively produce background work on Android, and
    collectAsState()
    for lightweight in-memory StateFlows or non-Android scopes.
  • Hoist Receivers & Listeners : Register system listeners and
    BroadcastReceiver
    s once at the screen level using
    DisposableEffect
    , not inside lazy list items.
  • Guard Optional Effects : Guard effects with
    if (callback != null)
    before launching to prevent allocating empty coroutines and effect nodes.
  • Prevent Stale Captures : Use
    rememberUpdatedState
    when long-running or looping coroutines in
    LaunchedEffect
    reference callbacks or parameters.
  • Replace Legacy Timers : Replace
    Handler.postDelayed
    or
    TimerTask
    with
    LaunchedEffect
    and
    delay()
    .
强制要求:在本类别中标记或修复任何问题之前,必须阅读 effects-and-threading.md。
  • 禁止在 Body 中直接执行副作用:绝不要在 composition 期间直接于 Composable body 中执行副作用;应将其包装在
    SideEffect
    、
    LaunchedEffect
    或用户事件回调中。
  • 禁止使用非挂起式
    LaunchedEffect
    和空的
    DisposableEffect
    :绝不要将
    LaunchedEffect
    用于同步操作(例如记录 view 日志或更新对象)。也绝不要将带有空
    onDispose {}
    块的
    DisposableEffect
    作为权宜之计;应改用
    SideEffect
    (在 Compose 1.12+ 中,
    SideEffect(keys) { ... }
    直接支持 key;在 Compose
    < 1.12
    中,
    SideEffect
    不接受参数,因此应检查项目的 Compose 版本,并使用
    SideEffect { ... }
    内部经过
    remember
    的 holder 跟踪 key 变化)。
  • 缓存 Body 中的分配:将重量级对象分配(如日期格式化器或复杂路径)和高成本计算包装在
    remember
    中。
  • 合并相关的
    LaunchedEffect
    :将多个
    LaunchedEffect(Unit)
    合并为一个
    LaunchedEffect
    ,并使用嵌套的
    launch { ... }
    块,以减少 effect 节点分配和协程启动器开销。
  • Effect 执行顺序与清理成本:遵守 effect 执行顺序(
    DisposableEffect
    >
    SideEffect
    > Layout 和 Draw > frame 后的
    LaunchedEffect
    )。注意,取消
    LaunchedEffect
    会产生协程取消和清理开销;在动画或滚动期间,避免频繁循环变更
    LaunchedEffect
    的 key。
  • 将工作移出主线程:使用
    produceState
    和
    withContext(Dispatchers.IO)
    ,将阻塞式 I/O、重量级文件解析和数据库操作移出主线程。
  • Lazy layout 中禁止 item 级曝光 Effect:绝不要在 lazy item 内使用
    LaunchedEffect
    或
    DisposableEffect
    记录曝光事件。Lazy item 会在进入视口前预先 composition,从而导致过早或错误的曝光记录;应改用带停留时长阈值的
    Modifier.onVisibilityChanged
    。
  • 生命周期感知的 Flow 收集:对于在 Android 上会主动产生后台工作的 UI Flow collection,使用
    collectAsStateWithLifecycle()
    ;对于轻量级内存
    StateFlow
    或非 Android scope,使用
    collectAsState()
    。
  • 上提 Receiver 与 Listener:使用
    DisposableEffect
    在 screen 层级仅注册一次系统 listener 和
    BroadcastReceiver
    ,不要在 lazy list item 内注册。
  • 保护可选 Effect:启动 effect 前使用
    if (callback != null)
    进行判断,以防止分配空协程和 effect 节点。
  • 防止捕获过期值:当
    LaunchedEffect
    中的长时间运行或循环协程引用 callback 或参数时,使用
    rememberUpdatedState
    。
  • 替换旧式计时器:使用
    LaunchedEffect
    和
    delay()
    替换
    Handler.postDelayed
    或
    TimerTask
    。

6. Graphics and custom drawing (references/graphics-performance.md)

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

MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/graphics-performance.md.
  • Cache Drawing Allocations : Replace decorative
    Canvas
    nodes with
    Modifier.drawWithCache
    to cache
    Path
    ,
    Paint
    , and
    Brush
    allocations.
  • Prefer AsyncImage instead of Image : Use Coil's
    AsyncImage
    for loading images from the internet and
    painterResource
    for drawables.
    Image
    is not optimized for caching and loading images and may cause performance issues.
  • Scope Fast-Changing Uniforms :
    remember
    the
    RuntimeShader
    and
    ShaderBrush
    in composition, update size uniforms in
    drawWithCache
    , and per-frame uniforms in
    onDrawBehind
    .
  • Graphics Path Object Reuse : Prefer
    Path.rewind()
    over
    reset()
    in hot draw loops to retain allocated buffers without reallocation.
  • Value Classes & Primitive Collections : Avoid generic tuples (
    Pair
    ,
    Triple
    ) and generic sets (
    HashSet<Long>
    ); use
    androidx.collection.MutableLongSet
    and
    androidx.compose.ui.util.packInts
    .
强制要求:在本类别中标记或修复任何问题之前,必须阅读 references/graphics-performance.md。
  • 缓存绘制分配:使用
    Modifier.drawWithCache
    替换装饰性
    Canvas
    节点,以缓存
    Path
    、
    Paint
    和
    Brush
    分配。
  • 优先使用
    AsyncImage
    而不是
    Image
    :使用 Coil 的
    AsyncImage
    加载网络图片,使用
    painterResource
    加载 drawable。
    Image
    未针对缓存和图片加载进行优化,可能引发性能问题。
  • 划分快速变化 Uniform 的更新作用域:在 composition 中
    remember
    RuntimeShader
    和
    ShaderBrush
    ,在
    drawWithCache
    中更新尺寸 uniform,在
    onDrawBehind
    中更新每帧 uniform。
  • 复用图形 Path 对象:在高频绘制循环中优先使用
    Path.rewind()
    而不是
    reset()
    ,以保留已分配的 buffer 并避免重新分配。
  • Value class 与基本类型集合:避免使用泛型 tuple(
    Pair
    、
    Triple
    )和泛型集合(
    HashSet<Long>
    );改用
    androidx.collection.MutableLongSet
    和
    androidx.compose.ui.util.packInts
    。

7. Custom modifiers:
Modifier.Node
(Custom modifiers guide)

7. 自定义 Modifier:
Modifier.Node
(自定义 modifier 指南)

  • Modifier.Node Migration : Migrate legacy
    Modifier.composed
    to
    ModifierNodeElement
    +
    Modifier.Node
    (or
    DelegatingNode
    when delegating to sub-nodes such as
    SuspendingPointerInputModifierNode
    ) with capability interfaces (
    DrawModifierNode
    ,
    LayoutModifierNode
    ,
    SemanticsModifierNode
    ,
    CompositionLocalConsumerModifierNode
    using
    currentValueOf(...)
    ) to eliminate per-node subcomposition overhead and enable node reuse (see Create custom modifiers with Modifier.Node):
    kotlin
    // 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)
        }
    }
    <br />
  • On-Demand Coordinates : Query
    requireLayoutCoordinates()
    on demand inside pointer or draw handlers in
    Modifier.Node
    rather than implementing
    GlobalPositionAwareModifierNode.onGloballyPositioned
    .
IMPORTANT : For more examples on how to migrate common modifiers to Modifier.Node, you must read Create custom modifiers with Modifier.Node.
  • 迁移到
    Modifier.Node
    :将旧式
    Modifier.composed
    迁移到
    ModifierNodeElement
    +
    Modifier.Node
    (当委托给
    SuspendingPointerInputModifierNode
    等子节点时使用
    DelegatingNode
    ),并使用能力接口(
    DrawModifierNode
    、
    LayoutModifierNode
    、
    SemanticsModifierNode
    、
    CompositionLocalConsumerModifierNode
    与
    currentValueOf(...)
    ),以消除每个节点的 subcomposition 开销并启用节点复用(参阅使用
    Modifier.Node
    创建自定义 modifier
    ):
    kotlin
    // 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)
        }
    }
    <br />
  • 按需获取坐标:在
    Modifier.Node
    的指针或绘制处理器内按需调用
    requireLayoutCoordinates()
    ,而不是实现
    GlobalPositionAwareModifierNode.onGloballyPositioned
    。
重要提示:如需更多常见 modifier 的迁移示例,必须阅读使用
Modifier.Node
创建自定义 modifier
。