Compose Performance & Stability Quiz
UI › Compose UI
A composable takes a data class with all 'val' properties of primitive/String types. With no extra annotations, how does the compiler classify it?
- Unstable, because user data classes are never inferred as stable
- Stable/immutable, so the composable can be skippable
- Stable only if you add @Stable manually
- Unstable unless the class implements equals()
Answer: Stable/immutable, so the composable can be skippable
The compiler infers a data class with only immutable val properties of stable types as stable/immutable automatically, making the consuming composable skippable without annotations.
Under Strong Skipping Mode, how are parameters whose type is unstable compared between recompositions?
- Using structural equality (equals())
- They are never compared; the composable always recomposes
- Using referential/instance equality (===)
- Using hashCode() only
Answer: Using referential/instance equality (===)
Strong Skipping makes such composables skippable but compares unstable parameters by instance equality (===); stable parameters still use equals().
Which change best avoids recomposition when an offset updates every frame during a drag?
- Modifier.offset { IntOffset(x.roundToPx(), y.roundToPx()) } reading state in the lambda
- Modifier.offset(x.dp, y.dp) reading the state values directly in composition
- Wrap the changing offset values in a remember block each frame
- Mark the whole composable @NonSkippableComposable to prevent it
Answer: Modifier.offset { IntOffset(x.roundToPx(), y.roundToPx()) } reading state in the lambda
The lambda overload defers the state read to the layout phase, so frequent changes invalidate layout/draw only and skip recomposition; the value-based overload reads in composition and recomposes each frame.
Why does Compose treat a List<Item> parameter as unstable by default?
- Lists are too large for Compose to compare efficiently on every change
- List is an interface, so the compiler can’t prove it’s immutable
- Lists always create a fresh instance during every recomposition pass
- Generics are inherently unstable in Kotlin, even for simple types
Answer: List is an interface, so the compiler can’t prove it’s immutable
List/Set/Map are interfaces; the compiler can't prove the backing implementation won't mutate, so it conservatively marks them unstable. kotlinx immutable collections solve this.
What is a key advantage of collectAsStateWithLifecycle over collectAsState on Android?
- It automatically converts cold upstream flows into hot flows
- It guarantees the latest value is delivered synchronously on the main thread
- It removes the requirement to supply an initial fallback value
- It stops collecting once the lifecycle falls below STARTED, sparing background work
Answer: It stops collecting once the lifecycle falls below STARTED, sparing background work
collectAsStateWithLifecycle ties collection to the lifecycle (at least STARTED by default), cancelling upstream work in the background; collectAsState keeps collecting while the composition is active.
Which statement about Strong Skipping Mode (Compose Compiler 2.0, Kotlin 2.0.20+) is correct?
- It is opt-in and stays disabled by default in current toolchains
- Enabled by default; it also auto-remembers lambdas keyed by their captures
- It makes every composable, including non-restartable ones, skippable
- It removes the need for snapshot state in composables entirely
Answer: Enabled by default; it also auto-remembers lambdas keyed by their captures
Strong Skipping is on by default since Kotlin 2.0.20+, makes restartable composables skippable regardless of param stability, and auto-wraps lambdas in remember keyed by their captures.
You suspect a list item recomposes too often. Which is the most appropriate first diagnostic step?
- Add @Immutable to every class in the module before re-measuring
- Switch to a debug build so the logs are easier to read and compare
- Use Layout Inspector recomposition counts and Compose compiler reports
- Replace LazyColumn with a regular Column to cut composition overhead
Answer: Use Layout Inspector recomposition counts and Compose compiler reports
Layout Inspector recomposition counts pinpoint hotspots and compiler reports reveal which composables are skippable/restartable and which classes are unstable, guiding a targeted fix; benchmarking is done on release builds.
A composable reads listState.firstVisibleItemIndex directly to compute a boolean isScrolled. Wrapping that computation in derivedStateOf improves performance because:
- it makes the surrounding composable non-restartable so it never recomposes
- it memoizes the boolean across configuration changes like rememberSaveable
- it shifts the state read into the layout phase instead of composition
- the observing scope recomposes only when the boolean flips, not every scroll frame
Answer: the observing scope recomposes only when the boolean flips, not every scroll frame
derivedStateOf recomputes whenever its reads change but only notifies observers when the resulting value changes, so scrolling that does not flip the boolean produces no recomposition.
You depend on a model class from a third-party library that the compiler marks unstable, and you cannot edit or annotate its source. What is the recommended fix?
- Use a Compose stability config file to mark the class stable
- Wrap each use in remember so the compiler treats it as stable
- Subclass it and put @Immutable on the subclass instead
- There is no real fix; you must fork the library to add it
Answer: Use a Compose stability config file to mark the class stable
The Compose compiler supports a stability configuration file (stabilityConfigurationFiles) where you list classes, including wildcard patterns, to be treated as stable when you cannot annotate the original source.
Providing a stable, unique key for each item in a LazyColumn primarily helps performance and correctness by:
- stopping the whole list from ever recomposing when data updates
- letting Compose match items across data changes so scroll and state survive
- automatically marking the item's backing data class as @Immutable
- disabling the list's item add and remove animations to save frames
Answer: letting Compose match items across data changes so scroll and state survive
Keys let Compose identify items across insertions, removals, and moves, so remembered state is preserved and items that did not change are not needlessly recomposed or recreated.
What does annotating a type @Immutable promise that @Stable does not?
- That the type properly implements both equals() and hashCode()
- That its instances are safe to share freely across multiple threads
- That public properties never change post-construction, so they never need re-reading
- That the type may only be used as a parameter to @Composable functions
Answer: That public properties never change post-construction, so they never need re-reading
@Immutable is the stronger contract: it guarantees public properties never change after construction, whereas @Stable permits mutation as long as Compose is notified of every change via snapshot state.
Why should Compose performance be benchmarked on a release (R8/minified) build rather than a debug build?
- Debug builds disable recomposition altogether, which quietly hides jank
- Release builds skip stability inference, so the resulting numbers look cleaner
- Layout Inspector recomposition counts work only on optimized release builds
- Debug builds are unoptimized with extra runtime checks, so timings look artificially slow
Answer: Debug builds are unoptimized with extra runtime checks, so timings look artificially slow
Debug builds are not optimized by R8 and carry extra runtime checks and instrumentation that exaggerate cost; representative measurements, along with Baseline Profiles, require a release build.
You pass a list created with mutableStateListOf() (a SnapshotStateList) to a composable. How does the compiler classify this parameter?
- Unstable, just like a plain ArrayList, so it always recomposes
- Stable, because Compose tracks its snapshot-backed mutations
- Stable only if it is also marked with @Immutable by the caller
- Rejected at compile time, since mutable lists cannot be passed
Answer: Stable, because Compose tracks its snapshot-backed mutations
SnapshotStateList is backed by the snapshot system, so Compose observes its mutations and treats it as stable, unlike a plain MutableList or ArrayList which is inferred unstable.
What problem does movableContentOf (movableContent) solve?
- It defers state reads from composition down to the draw phase instead
- It forces a composable to recompose on every frame for smooth animation
- It keeps a composable's remembered state when it changes layout position
- It flags an entire list of items as stable in one single call
Answer: It keeps a composable's remembered state when it changes layout position
movableContentOf lets the same content be relocated within the composition while keeping its remembered state and underlying nodes alive, instead of disposing and recreating them when its parent or position changes.