Stitch Fix Android
The Compose Recomposition Trap
The team adopted Jetpack Compose to move faster. Instead, the UI recomposed on every state change — even when nothing visible had changed.
The Incident
Stitch Fix's Android team adopted Jetpack Compose in 2022 and immediately hit production UI jank. Their feed — a list of styled clothing items with prices, ratings, and interactive actions — was recomposing far more than expected. The Recomposition Highlighter lit up like a Christmas tree. Three distinct bugs, each teaching a different lesson about how Compose's stability system actually works.
Evidence from the Scene
- Recomposition Highlighter showed list items recomposing on every user action — even unrelated ones
- A ViewModel instance was captured inside a lambda passed to list items
- Domain model classes (Product, Price, Rating) lived in a non-Compose Gradle module
- Scroll position was read directly in a composable that also displayed a 'scroll to top' button
- The Layout Inspector showed recomposition counts in the hundreds after 10 seconds of use
- Moving a data class from :domain to :ui module mysteriously fixed recomposition on that screen
The Suspects
3 of these are the real root causes. The others are plausible-sounding distractors.
Lambda capturing a ViewModel instance, making it unstable and non-skippable
Domain model classes from a non-Compose module inferred as unstable by the Compose compiler
Scroll position read directly in a composable, causing recomposition on every scroll pixel
Missing key parameter in LazyColumn items causing full list recomposition on any change
Using MutableList instead of ImmutableList as the type for list composables
The Verdict
Real Root Causes
Lambda capturing a ViewModel instance, making it unstable and non-skippable
ViewModel is not a stable type in Compose's eyes. A lambda that captures a ViewModel reference is itself unstable — Compose cannot guarantee it hasn't changed, so composables receiving it never skip recomposition. The fix: use method references or remember { } to stabilize the lambda.
Domain model classes from a non-Compose module inferred as unstable by the Compose compiler
Compose's stability inference only applies within modules that have the Compose compiler plugin enabled. Domain model classes in a pure Kotlin :domain module are always inferred as unstable — the compiler has no proof they haven't changed. The fix: map to @Stable UI model classes at the Compose boundary.
Scroll position read directly in a composable, causing recomposition on every scroll pixel
Reading LazyListState.firstVisibleItemIndex directly in a composable that shows a 'scroll to top' button caused the entire composable to recompose on every scroll pixel. derivedStateOf { listState.firstVisibleItemIndex > 0 } recomposes only when the boolean result changes.
Plausible But Wrong
Missing key parameter in LazyColumn items causing full list recomposition on any change
Missing keys cause all items to recompose on list changes — a real issue, but not what the clues describe. The ViewModel capture and cross-module instability are the root causes here.
Using MutableList instead of ImmutableList as the type for list composables
MutableList is unstable in Compose and would cause recomposition — but the clues specifically point to lambda capture and cross-module inference as the culprits here, not the collection type.
Summary
Stitch Fix hit three distinct Compose stability bugs in one codebase: unstable lambda capture, cross-module stability inference failure, and unguarded scroll state reads. Each represents a different facet of Compose's compile-time stability system. The fixes — remember { }-wrapped lambdas, @Stable boundary mapping, and derivedStateOf — are now standard Compose performance practice. Published on the Stitch Fix engineering blog in August 2022, it remains one of the most cited real-world Compose performance post-mortems.
The Real Decision That Caused This
“Adopting Compose without understanding that stability is a compile-time contract, that module boundaries break stability inference, and that every state read in a composable creates a recomposition subscriber.”
Lesson Hint
Chapter 3 (Jetpack Compose) covers stability, @Stable, @Immutable, derivedStateOf, and the Recomposition Highlighter. Chapter 7 (Platform & Performance) covers profiling tools including Layout Inspector.
Want to test yourself before reading the verdict?
Open Interactive Case in Autopsy Lab