jetpack-compose-performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFoundational prerequisites
基础前提
Before micro-optimizing composables, ensure standard production flags are in
place:
- Release Build: Never profile or benchmark debuggable builds; ART runtime verification severely degrades performance.
- R8 Minification: Keep R8 enabled to optimize bytecode and coroutine dispatch.
- 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 进行微优化之前,确保标准生产环境构建标志已正确配置:
- Release Build:切勿对可调试构建进行性能分析或基准测试;ART 运行时验证会严重降低性能。
- R8 Minification:保持启用 R8,以优化字节码和协程调度。
- Baseline Profiles:为关键用户旅程(冷启动、关键滚动信息流)生成 baseline profile,使 ART VM 能够提前预编译和预加载类。
Optimization audit workflow
优化审计工作流
- Exhaustive Scan : Scan the target code against all 7 core categories in the following sections. Evaluate every function, modifier chain, and lazy-layout data model against all checklist rules rather than stopping after the first issue found.
@Composable - Mandatory Reference Read Before Flagging or Editing : Whenever a file matches a rule in Categories 1--7, you MUST open and read the corresponding 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.
references/*.md
- 全面扫描:依据后续各节中的全部 7 个核心类别扫描目标代码。应根据所有检查清单规则评估每一个 函数、modifier 链和 lazy layout 数据模型,而不是在发现第一个问题后便停止。
@Composable - 标记问题或编辑前强制阅读参考文档:每当文件符合类别 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 or
Modifier.offset) so reads occur in Layout or Draw phase rather than Composition.Modifier.graphicsLayer - 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 (, or
Read -> 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 (Composition Read -> Layout or Draw WriteorWrite -> Read) as a backwards write---a backwards write strictly requires a read of that state to precede the write.Composition Write -> Draw Read - Avoid Redundant State Wrapping in Composition () : Do not wrap synchronous values in
Write -> Readonly to overwrite them immediately at the top of composition before reading them (for example,remember { mutableStateOf(...) }). While not a backwards write, this adds unnecessary slot table andvar path by remember { mutableStateOf(Path()) }; path = viewModel.getPath()snapshot overhead (and risks becoming a backwards write if a read is later added preceding the assignment); use a plain localMutableStateor read the value directly inside the target phase (such asval).Modifier.drawWithCache - Explicit Keys & Slot Overhead : Always pass all input variables and composable parameters referenced inside
rememberas calculation keys. Remember thatrememberstoresrememberobjects in the slot table and checks key equality on every recomposition; onlykeys + 1when allocations are heavy or identity must be preserved. Do not pass snapshot state values/properties (e.g.remember) as keys when wrappingstate.value(passing thederivedStateOfinstance itself is fine if the instance reference can change).State - vs
computedStateOf(Compose 1.13+) : On Compose 1.13+, usederivedStateOffor simple condition checks and filtered state from fast-changing inputs (such ascomputedStateOf), which provides significantly faster invalidation thanscrollState.value > 0(on ComposederivedStateOf, use< 1.13). Reserveremember { derivedStateOf { ... } }for heavy calculations that benefit from multi-read caching, or trivial calculations read frequently enough per composition (roughlyderivedStateOftimes without invalidating between reads) to make up for the invalidation cost.10 / numberOfStatesReadPerCalculation - Synchronous State Derivation : Audit for asynchronous +
produceStateblocks observing Compose states (such as scroll positions or collection sizes); replace them with synchronoussnapshotFlow(orremember { derivedStateOf { ... } }on Compose 1.13+).computedStateOf - Trivial Primitives : Prefer plain custom Kotlin getters () over
get()orderivedStateOffor simple boolean expressions and property lookups.computedStateOf - Collection State Tracking : Back whole-list updates with standard and
mutableListOf()with load-bearing assignments instead ofmutableStateOf(list, neverEqualPolicy()).mutableStateListOf() - Snapshot Collections : Use for repeated queries or iterations to avoid per-element read observation overhead.
SnapshotStateList.toList() - Repeated State Reads : Extract repeated Compose property reads into a local variable at the top of the block.
State - State in Setters : Avoid reading Compose inside custom property setters; use a raw backing field when triggering change side-effects and batch multi-property updates.
State - Stability & Skipping Trade-Offs (Skipping Isn't Free) : Do not fixate on stability or treat ,
@Immutable, or@Stableas 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 standardImmutableList) when instance equality (kotlin.collections.List) matches. Annotating models or using===forces structural equality (ImmutableList), which can be significantly more expensive than recomposition itself for large collections or complex classes..equals() - Never Add ,
@Stable, or@Immutableto Collection Models : When writing or optimizing code, never addImmutableListor@Immutableto models containing collections (@Stable,List,Set), and never recommend replacingMapwithkotlin.collections.List<T>(or addingImmutableList<T>toImmutableList). Callingstability_config.txtO(N)on collections on every recomposition pass is more expensive than.equals()instance equality (O(1)) under Strong Skipping Mode.=== - Scope Guard for Removing Existing /
@StableAnnotations (Large Lazy Datasets Only) :@Immutable- When to Remove : Only recommend removing existing or
@Stableannotations from data models when both conditions are met: (1) the model is used within a lazy layout (@Immutable,LazyColumn,LazyRow,LazyGrid,Pager) 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 likeScalingLazyColumnwithPostandList<Paragraph>), where recursiveList<Markup>O(N)checks degrade scrolling and recomposition performance..equals() - When to Leave Untouched : For all other existing ,
@Stable, or@Immutableusages 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 allImmutableList/@Stableclasses across a codebase without verifying they feed a lazy layout with a large dataset.@Immutable
- When to Remove : Only recommend removing existing
- When to Add /
@StableUnder Strong Skipping Mode : Flat@Immutablees withdata classprimitive/valproperties in Compose modules are already inferred as stable by the compiler and useStringautomatically---do not add redundant.equals()annotations to them. Only add@Immutableor@Stablewhen 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 (@Immutable,List,Set), and itsMapcheck is trivial.equals().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),使读取发生在 Layout 或 Draw 阶段,而不是 Composition 阶段。Modifier.graphicsLayer - 禁止反向写入(验证读取先于写入的顺序):绝不要在更晚的阶段修改状态,或在同一 composition pass 中读取状态后才修改状态(,或
Read -> Write)。防止跨阶段反向写入,以避免首帧跳变、卡顿和无限 recomposition 循环(Backwards Write)。不要将 composable 开头的状态赋值标记为反向写入——反向写入严格要求先读取该状态,再执行写入。Composition Read -> Layout or Draw Write - 避免在 Composition 中重复包装状态():不要仅仅为了在 composition 顶部读取前立即覆盖同步值,就将其包装在
Write -> Read中(例如remember { mutableStateOf(...) })。这虽然不属于反向写入,但会增加不必要的 slot table 和var path by remember { mutableStateOf(Path()) }; path = viewModel.getPath()snapshot 开销(如果在赋值前新增读取,还可能变成反向写入);应改用普通局部MutableState,或直接在目标阶段(如val)内读取该值。Modifier.drawWithCache - 显式 键与 Slot 开销:始终将
remember内部引用的所有输入变量和 composable 参数作为计算键传入。需注意,remember会在 slot table 中存储remember个对象,并在每次 recomposition 时检查键的相等性;仅当对象分配成本较高或必须保持标识时才使用keys + 1。使用remember包装时,不要将 snapshot 状态值或属性(如derivedStateOf)作为键传入;传入state.value实例本身即可,前提是实例引用可能会发生变化。State - 与
computedStateOf(Compose 1.13+):在 Compose 1.13+ 中,对于简单条件检查以及从快速变化输入中筛选出的状态(如derivedStateOf),使用scrollState.value > 0,其失效速度显著快于computedStateOf(在 ComposederivedStateOf中,使用< 1.13)。仅将remember { derivedStateOf { ... } }用于可从多次读取缓存中获益的重计算,或读取频率足够高的简单计算,即每次 composition 大约读取derivedStateOf次且读取之间未发生失效,从而抵消失效成本。10 / numberOfStatesReadPerCalculation - 同步状态派生:检查异步 +
produceState块中对 Compose 状态(如滚动位置或集合大小)的观察;将其替换为同步的snapshotFlow(或在 Compose 1.13+ 中使用remember { derivedStateOf { ... } })。computedStateOf - 简单原语:对于简单布尔表达式和属性查找,优先使用普通自定义 Kotlin getter(),而不是
get()或derivedStateOf。computedStateOf - 集合状态跟踪:对于整个列表的更新,使用标准 配合
mutableListOf(),并通过关键赋值触发更新,而不要使用mutableStateOf(list, neverEqualPolicy())。mutableStateListOf() - Snapshot 集合:对于重复查询或遍历,使用 ,以避免按元素读取所产生的观察开销。
SnapshotStateList.toList() - 重复状态读取:将重复的 Compose 属性读取提取到块顶部的局部变量中。
State - Setter 中的状态:避免在自定义属性 setter 中读取 Compose ;触发变更副作用时应使用原始 backing field,并批量执行多属性更新。
State - 稳定性与跳过之间的权衡(跳过并非没有成本):不要过度关注稳定性,也不要将 、
@Immutable或@Stable视为默认解决方案。跳过 composable 并非没有成本:每个参数都会像被ImmutableList一样存储在 slot table 中,并且在每次 recomposition 时必须比较所有输入。在实例相等性(remember)匹配时,Strong Skipping Mode 已经能够跳过具有不稳定参数(包括标准===)的 composable。给模型添加注解或使用kotlin.collections.List会强制采用结构相等性(ImmutableList);对于大型集合或复杂类,这可能比 recomposition 本身昂贵得多。.equals() - 绝不要给集合模型添加 、
@Stable或@Immutable:编写或优化代码时,绝不要给包含集合(ImmutableList、List、Set)的模型添加Map或@Immutable,也不要建议将@Stable替换为kotlin.collections.List<T>(或将其添加到ImmutableList<T>)。在 Strong Skipping Mode 下,每次 recomposition 都对集合执行stability_config.txt的O(N),成本会高于.equals()的实例相等性检查(O(1))。=== - 移除现有 /
@Stable注解的适用范围约束(仅限大型 Lazy 数据集):@Immutable- 何时移除:仅当同时满足以下两个条件时,才建议从数据模型中移除现有的 或
@Stable注解:(1) 该模型在 lazy layout(@Immutable、LazyColumn、LazyRow、LazyGrid、Pager)中作为输入数据源或 item 模型使用;且 (2) 该模型包含大型数据集或深层嵌套集合层级(例如信息流、大型标签或 item 列表,或类似ScalingLazyColumn包含Post和List<Paragraph>的多层嵌套列表),其中递归的List<Markup>O(N)检查会降低滚动和 recomposition 性能。.equals() - 何时保持不变:对于代码库中其他所有现有的 、
@Stable或@Immutable用法——例如非 lazy UI 模型、独立组件,或包含小型、有界集合的模型(例如包含少量徽章、缩略图或元数据行的卡片)——如果数据集规模较小或未知,应保持现有注解不变且不要将其标记为问题。绝不能在整个代码库中盲目搜索并标记所有ImmutableList/@Stable类,而未验证其是否为大 lazy layout 提供大型数据集。@Immutable
- 何时移除:仅当同时满足以下两个条件时,才建议从数据模型中移除现有的
- 在 Strong Skipping Mode 下何时添加 /
@Stable:Compose 模块中仅包含@Immutable基本类型或val属性的扁平String已由编译器推断为稳定类型,并自动使用data class——不要为其添加冗余的.equals()注解。仅当编译器无法推断稳定性时(例如来自非 Compose 模块的类或接口),且数据源(如 Room 或 MVI reducer)在每次发射时都会为未变化的 item 分配新对象实例,才应添加@Immutable或@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 ; use
onGloballyPositionedfor viewport tracking,Modifier.onVisibilityChangedorModifier.onPlacedfor local bounds, and queryModifier.onLayoutRectChangedon demand insiderequireLayoutCoordinates()event handlers.Modifier.Node - Anchor Floating Content via Custom : When attaching floating labels or tooltips to a component, use a custom
Layoutmeasuring and placing both children relative to each other. Never trackLayoutcoordinates inonGloballyPositionedto position floating content withMutableState, which causes a 1-frame lag and schedules a second composition pass.offset - Phase Deferral for Offsets : Wrap dynamic offsets in lambdas () to skip Composition straight to Layout.
Modifier.offset { IntOffset(...) } - Tree Depth & Decorative Wrappers : Avoid redundant wrapper es and decorative
Boxnodes; attach modifiers directly or useCanvas.Modifier.drawBehind - Subcomposition Overhead : Avoid /
BoxWithConstraintsinside hot paths and lazy items; prefer standard modifiers,SubcomposeLayout, orModifier.layout.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() - 通过自定义 锚定浮动内容:将浮动标签或 tooltip 附加到组件时,使用自定义
Layout相对彼此测量和放置两个子节点。绝不要在Layout中跟踪MutableState坐标并使用onGloballyPositioned定位浮动内容,否则会导致 1 帧延迟并调度第二次 composition pass。offset - 偏移的阶段延迟:将动态偏移包装在 lambda 中(),以跳过 Composition 并直接进入 Layout 阶段。
Modifier.offset { IntOffset(...) } - 树深度与装饰性包装器:避免冗余的包装 和装饰性
Box节点;应直接附加 modifier,或使用Canvas。Modifier.drawBehind - Subcomposition 开销:避免在热路径和 lazy item 中使用 /
BoxWithConstraints;优先使用标准 modifier、SubcomposeLayout或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 and
keyapply strictly to Lazy containers (contentType,LazyColumn,LazyRow). Never flag standard non-lazy containers (LazyGrid,Column,Row) iterating over collections.Box - Mandatory and Primitive
contentType: Always specify both primitivekeys (key,Int,Long) andStrings for all items in lazy layouts (contentType,LazyColumn,LazyRow). When usingLazyGridoritems, never omititemsIndexed:contentType. OmittingcontentType = { _, item -> item.type }prevents Compose from reusing composition slots during scrolling.contentType - In-Item Index Lookups : Replace with
list.indexOf(item)to eliminateitemsIndexedlookups during layout passes.O(N²) - Observable Selection & State Queries : Convert selection tracking from to an immutable
mutableStateListOfinSet(MutableState),mutableStateOf(emptySet<String>()), or Map formutableStateSetOf()lookups. Pass lambda providers (O(1)) to leaf composables to isolate recomposition from parentisSelected: () -> Booleanscopes.items - Propagate CompositionLocals in Lazy Items : Avoid querying s (
CompositionLocal,LocalContext.current,LocalDensity.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.LocalSharedTransitionScope.current - Defer Shared Transitions in Lazy Items : Put or
Modifier.sharedBoundson lazy items behind visibility indicators (such asModifier.sharedElementor viewport visibility checks) so offscreen or pre-fetched items do not instantiate transition scopes.Modifier.onVisibilityChanged - Item Count Derivations : Read collection directly instead of wrapping list counts in
.size.derivedStateOf - Small & Bounded Lists : Use standard or
RowwithColumnorforloops for small or bounded collections (e.g. 1 to 10 items) to eliminate lazy virtualization, measuring, and pre-fetching overhead.forEach - Hoist Shared Flows : Collect shared s once in the parent container preceding lazy layouts; never call
Flowper visible item.collectAsStateWithLifecycle() - Avoid Side Effects in Pre-Fetched Items : Never trigger analytics in inside lazy items; use
LaunchedEffect(Unit)with dwell thresholds instead.Modifier.onVisibilityChanged
强制要求:在本类别中标记或修复任何问题之前,必须阅读 references/lazy-layouts.md。
- Lazy layout 适用范围约束:有关 和
key的指令严格适用于 lazy 容器(contentType、LazyColumn、LazyRow)。绝不要将遍历集合的标准非 lazy 容器(LazyGrid、Column、Row)标记为问题。Box - 必须提供 和基本类型
contentType:始终为 lazy layout(key、LazyColumn、LazyRow)中的所有 item 同时指定基本类型LazyGrid(key、Int、Long)和String。使用contentType或items时,绝不能省略itemsIndexed:contentType。省略contentType = { _, item -> item.type }会导致 Compose 在滚动期间无法复用 composition slot。contentType - Item 内索引查找:将 替换为
list.indexOf(item),以消除布局阶段中的itemsIndexed查找。O(N²) - 可观察选择与状态查询:将选择跟踪从 转换为
mutableStateListOf中的不可变MutableState(Set)、mutableStateOf(emptySet<String>())或mutableStateSetOf(),以实现Map查找。将 lambda provider(O(1))传递给叶子 composable,以将 recomposition 与父级isSelected: () -> Booleanscope 隔离。items - 在 Lazy item 中传递 CompositionLocals:避免在各个 lazy item composable 内部查询 (如
CompositionLocal、LocalContext.current、LocalDensity.current、scope 等);应在父容器或 screen 中解析一次,再将解析后的值作为参数向下传递。LocalSharedTransitionScope.current - 延迟 Lazy item 中的共享转场:仅在满足可见性条件时(例如使用 或视口可见性检查)才在 lazy item 上附加
Modifier.onVisibilityChanged或Modifier.sharedBounds,避免屏幕外或预取 item 创建 transition scope。Modifier.sharedElement - Item 数量派生:直接读取集合的 ,不要将列表数量包装在
.size中。derivedStateOf - 小型且有界的列表:对于小型或有界集合(例如 1 到 10 个 item),使用带 或
for循环的标准forEach或Row,以消除 lazy 虚拟化、测量和预取开销。Column - 上提共享 Flow:在父容器中、lazy layout 之前仅收集一次共享 ;绝不要对每个可见 item 调用
Flow。collectAsStateWithLifecycle() - 避免在预取 item 中触发副作用:绝不要在 lazy item 的 中触发 analytics;应改用带停留时长阈值的
LaunchedEffect(Unit)。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 to bypass Composition and evaluate directly in the Draw phase.
Modifier.graphicsLayer { ... } - Phase Deferral for Offsets & Size : Wrap animated offsets in and animate constraints using
Modifier.offset { IntOffset(...) }orModifier.layout { ... }to bypass recomposition during size transitions.Modifier.animateBounds - Defer Shared Transitions in Fast-Scrolling Lists : Attach or
Modifier.sharedBoundsonly when visible usingModifier.sharedElementto avoid modifier and observation instantiation costs on discarded pre-fetched items.Modifier.onVisibilityChanged - Lambda Providers : Pass lambda providers () instead of raw animated values to child composables to keep state reads deferred to Draw or Layout.
() -> Float - Persistent in Nodes : Store
Animatableas a persistent member property onAnimatableand launch inModifier.NodeusingonAttach().coroutineScope.launch
强制要求:在本类别中标记或修复任何问题之前,必须阅读 references/animation.md。
- 动画属性的阶段延迟:将快速变化的动画状态读取(alpha、scale、rotation、color)包装在 中,以绕过 Composition 并直接在 Draw 阶段求值。
Modifier.graphicsLayer { ... } - 偏移与尺寸的阶段延迟:将动画偏移包装在 中,并使用
Modifier.offset { IntOffset(...) }或Modifier.layout { ... }动画化约束,以在尺寸变化期间绕过 recomposition。Modifier.animateBounds - 在快速滚动列表中延迟共享转场:仅在 item 可见时使用 附加
Modifier.onVisibilityChanged或Modifier.sharedBounds,以避免对已丢弃的预取 item 产生 modifier 和观察实例化开销。Modifier.sharedElement - Lambda Provider:向子 composable 传递 lambda provider(),而不是原始动画值,以将状态读取延迟到 Draw 或 Layout 阶段。
() -> Float - 在 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, or user event callbacks.LaunchedEffect - Prohibit Non-Suspending & Empty
LaunchedEffect: Never useDisposableEffectfor synchronous operations (e.g. logging views or updating objects). Never useLaunchedEffectwith an emptyDisposableEffectblock as a workaround. UseonDispose {}instead (in Compose 1.12+,SideEffectsupports keys directly; on ComposeSideEffect(keys) { ... },< 1.12takes no arguments, so check the project's Compose version and track key changes with aSideEffected holder insideremember).SideEffect { ... } - Cache Allocations in Body : Wrap heavy object allocations (such as date formatters or complex paths) and expensive computations in .
remember - Combine Related s : Combine multiple
LaunchedEffectcalls into a singleLaunchedEffect(Unit)with nestedLaunchedEffectblocks to reduce effect node allocations and coroutine launcher overhead.launch { ... } - Effect Ordering & Teardown Costs : Respect effect execution order (>
DisposableEffect> Layout and Draw >SideEffectafter frame). Note that canceling aLaunchedEffectincurs coroutine cancellation and teardown overhead; avoid cyclingLaunchedEffectkeys frequently during animations or scrolling.LaunchedEffect - Main Thread Offloading : Offload blocking I/O, heavy file parsing, and database operations to using
withContext(Dispatchers.IO).produceState - No Item-Level Impression Effects in Lazy Layouts : Never use or
LaunchedEffectinside lazy items for impression logging. Lazy items are pre-composed before entering the viewport, causing premature or false impressions. UseDisposableEffectwith dwell thresholds instead.Modifier.onVisibilityChanged - Lifecycle-Aware Flow Collection : Use for UI Flow collections that actively produce background work on Android, and
collectAsStateWithLifecycle()for lightweight in-memory StateFlows or non-Android scopes.collectAsState() - Hoist Receivers & Listeners : Register system listeners and s once at the screen level using
BroadcastReceiver, not inside lazy list items.DisposableEffect - Guard Optional Effects : Guard effects with before launching to prevent allocating empty coroutines and effect nodes.
if (callback != null) - Prevent Stale Captures : Use when long-running or looping coroutines in
rememberUpdatedStatereference callbacks or parameters.LaunchedEffect - Replace Legacy Timers : Replace or
Handler.postDelayedwithTimerTaskandLaunchedEffect.delay()
强制要求:在本类别中标记或修复任何问题之前,必须阅读 effects-and-threading.md。
- 禁止在 Body 中直接执行副作用:绝不要在 composition 期间直接于 Composable body 中执行副作用;应将其包装在 、
SideEffect或用户事件回调中。LaunchedEffect - 禁止使用非挂起式 和空的
LaunchedEffect:绝不要将DisposableEffect用于同步操作(例如记录 view 日志或更新对象)。也绝不要将带有空LaunchedEffect块的onDispose {}作为权宜之计;应改用DisposableEffect(在 Compose 1.12+ 中,SideEffect直接支持 key;在 ComposeSideEffect(keys) { ... }中,< 1.12不接受参数,因此应检查项目的 Compose 版本,并使用SideEffect内部经过SideEffect { ... }的 holder 跟踪 key 变化)。remember - 缓存 Body 中的分配:将重量级对象分配(如日期格式化器或复杂路径)和高成本计算包装在 中。
remember - 合并相关的 :将多个
LaunchedEffect合并为一个LaunchedEffect(Unit),并使用嵌套的LaunchedEffect块,以减少 effect 节点分配和协程启动器开销。launch { ... } - Effect 执行顺序与清理成本:遵守 effect 执行顺序(>
DisposableEffect> Layout 和 Draw > frame 后的SideEffect)。注意,取消LaunchedEffect会产生协程取消和清理开销;在动画或滚动期间,避免频繁循环变更LaunchedEffect的 key。LaunchedEffect - 将工作移出主线程:使用 和
produceState,将阻塞式 I/O、重量级文件解析和数据库操作移出主线程。withContext(Dispatchers.IO) - Lazy layout 中禁止 item 级曝光 Effect:绝不要在 lazy item 内使用 或
LaunchedEffect记录曝光事件。Lazy item 会在进入视口前预先 composition,从而导致过早或错误的曝光记录;应改用带停留时长阈值的DisposableEffect。Modifier.onVisibilityChanged - 生命周期感知的 Flow 收集:对于在 Android 上会主动产生后台工作的 UI Flow collection,使用 ;对于轻量级内存
collectAsStateWithLifecycle()或非 Android scope,使用StateFlow。collectAsState() - 上提 Receiver 与 Listener:使用 在 screen 层级仅注册一次系统 listener 和
DisposableEffect,不要在 lazy list item 内注册。BroadcastReceiver - 保护可选 Effect:启动 effect 前使用 进行判断,以防止分配空协程和 effect 节点。
if (callback != null) - 防止捕获过期值:当 中的长时间运行或循环协程引用 callback 或参数时,使用
LaunchedEffect。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 nodes with
Canvasto cacheModifier.drawWithCache,Path, andPaintallocations.Brush - Prefer AsyncImage instead of Image : Use Coil's for loading images from the internet and
AsyncImagefor drawables.painterResourceis not optimized for caching and loading images and may cause performance issues.Image - Scope Fast-Changing Uniforms : the
rememberandRuntimeShaderin composition, update size uniforms inShaderBrush, and per-frame uniforms indrawWithCache.onDrawBehind - Graphics Path Object Reuse : Prefer over
Path.rewind()in hot draw loops to retain allocated buffers without reallocation.reset() - Value Classes & Primitive Collections : Avoid generic tuples (,
Pair) and generic sets (Triple); useHashSet<Long>andandroidx.collection.MutableLongSet.androidx.compose.ui.util.packInts
强制要求:在本类别中标记或修复任何问题之前,必须阅读 references/graphics-performance.md。
- 缓存绘制分配:使用 替换装饰性
Modifier.drawWithCache节点,以缓存Canvas、Path和Paint分配。Brush - 优先使用 而不是
AsyncImage:使用 Coil 的Image加载网络图片,使用AsyncImage加载 drawable。painterResource未针对缓存和图片加载进行优化,可能引发性能问题。Image - 划分快速变化 Uniform 的更新作用域:在 composition 中
remember和RuntimeShader,在ShaderBrush中更新尺寸 uniform,在drawWithCache中更新每帧 uniform。onDrawBehind - 复用图形 Path 对象:在高频绘制循环中优先使用 而不是
Path.rewind(),以保留已分配的 buffer 并避免重新分配。reset() - Value class 与基本类型集合:避免使用泛型 tuple(、
Pair)和泛型集合(Triple);改用HashSet<Long>和androidx.collection.MutableLongSet。androidx.compose.ui.util.packInts
7. Custom modifiers: Modifier.Node
(Custom modifiers guide)
Modifier.Node7. 自定义 Modifier:Modifier.Node
(自定义 modifier 指南)
Modifier.Node-
Modifier.Node Migration : Migrate legacyto
Modifier.composed+ModifierNodeElement(orModifier.Nodewhen delegating to sub-nodes such asDelegatingNode) with capability interfaces (SuspendingPointerInputModifierNode,DrawModifierNode,LayoutModifierNode,SemanticsModifierNodeusingCompositionLocalConsumerModifierNode) to eliminate per-node subcomposition overhead and enable node reuse (see Create custom modifiers with Modifier.Node):currentValueOf(...)<br />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) } } -
On-Demand Coordinates : Queryon demand inside pointer or draw handlers in
requireLayoutCoordinates()rather than implementingModifier.Node.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),以消除每个节点的 subcomposition 开销并启用节点复用(参阅使用currentValueOf(...)创建自定义 modifier):Modifier.Node<br />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) } } -
按需获取坐标:在的指针或绘制处理器内按需调用
Modifier.Node,而不是实现requireLayoutCoordinates()。GlobalPositionAwareModifierNode.onGloballyPositioned
重要提示:如需更多常见 modifier 的迁移示例,必须阅读使用 创建自定义 modifier。
Modifier.Node