Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Understanding Recomposition Performance Pitfalls

Jossi Wolf
November 15, 2022

Understanding Recomposition Performance Pitfalls

Jossi Wolf

November 15, 2022
Tweet

More Decks by Jossi Wolf

Other Decks in Programming

Transcript

  1. Related talks We go further into the details of why

    deferring reads of Compose state works, learn about stability and how Compose infers it, have a look at a new API for reportFullyDrawn, and more. A holistic guide to app performance with tips that apply to all Android apps. More performance tips for Jetpack Compose Modern App Performance @shikasd_ @jossiwolf
  2. Always test performance in release mode with R8 enabled —

    Ben Trengrove, twice! @shikasd_ @jossiwolf
  3. @Composable fun Example() { var counter by remember { mutableStateOf(0)

    } Text( modifier = Modifier.clickable { counter ++ }, text = "$counter" ) } @shikasd_ @jossiwolf
  4. @Composable fun Example() { var counter by remember { mutableStateOf(0)

    } Text( modifier = Modifier.clickable { counter ++ }, text = "$counter" ) } state value gets accessed @shikasd_ @jossiwolf
  5. @Composable fun Example($composer: Composer, ... ) { $composer.startRestartGroup(FunctionKey) // function

    body $composer.endRestartGroup() ?. updateScope { $composer -> Example($composer, ... ) } } Recompose scope @shikasd_ @jossiwolf
  6. @Composable fun Example() { var counter by remember { mutableStateOf(0)

    } Text( modifier = Modifier.clickable { counter ++ }, text = "$counter" ) } state value read inside Example function Composition will remember that. ? @shikasd_ @jossiwolf
  7. @Composable fun Example() { var counter by remember { mutableStateOf(0)

    } Text( modifier = Modifier.clickable { counter ++ }, text = "$counter" ) } state value gets updated @shikasd_ @jossiwolf
  8. • State is backed by Snapshot 📷 system • State

    changes in Snapshot are transactional and atomic (+ observers!) • Recomposer observes state changes through Snapshot system @shikasd_ @jossiwolf
  9. @Composable fun Example($composer: Composer, ... ) { $composer.startRestartGroup(FunctionKey) // function

    body $composer.endRestartGroup() ?. updateScope { $composer -> Example($composer, ... ) } } Recompose scope @shikasd_ @jossiwolf
  10. @Composable fun Example() { var counter by remember { mutableStateOf(0)

    } Text( modifier = Modifier.clickable { counter ++ }, text = "$counter" ) } Composition DID remember that. ? Recompose scope state value gets accessed @shikasd_ @jossiwolf
  11. @Composable fun App() { var scrollOffset by remember { mutableStateOf(0f)

    } val scrollableState = rememberScrollableState( onScroll = { delta -> scrollOffset = maxOf(scrollOffset + delta, 0f) delta } ) Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = scrollOffset > 0f) Footer() } } @shikasd_ @jossiwolf
  12. @Composable fun App() { var scrollOffset by remember { mutableStateOf(0f)

    } val scrollableState = rememberScrollableState( onScroll = { delta -> scrollOffset = maxOf(scrollOffset + delta, 0f) delta } ) Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = scrollOffset > 0f) Footer() } } @shikasd_ @jossiwolf
  13. @Composable fun App() { var scrollOffset by remember { mutableStateOf(0f)

    } val scrollableState = rememberScrollableState( onScroll = { delta -> scrollOffset = maxOf(scrollOffset + delta, 0f) delta } ) Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = scrollOffset > 0f) Footer() } } @shikasd_ @jossiwolf
  14. @Composable fun App() { var scrollOffset by remember { mutableStateOf(0f)

    } val scrollableState = rememberScrollableState( onScroll = { delta -> scrollOffset = maxOf(scrollOffset + delta, 0f) … } ) Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = scrollOffset > 0f) Footer() } } state read! @shikasd_ @jossiwolf
  15. @Composable fun App() { var scrollOffset by remember { mutableStateOf(0f)

    } ... Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = { scrollOffset > 0f }) Footer() } } @Composable fun Content(showScrollToTop: () -> Boolean) { if (showScrollToTop()) { … } } // perf tip #1: defer reads state read! @shikasd_ @jossiwolf
  16. @Composable fun App() { var scrollOffset = remember { mutableStateOf(0f)

    } ... Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = { scrollOffset > 0f }) Footer() } } @Composable fun Content(showScrollToTop: State<Boolean>) { ... } // perf tip #2: defer reads ❌
  17. / / perf tip #2: extract State read out of

    composition @shikasd_ @jossiwolf
  18. Composition Layout Draw // perf tip #1: extract State read

    out of composition @shikasd_ @jossiwolf
  19. @Composable fun Example() { var state by remember { mutableStateOf(0)

    } Text( "$state" // read in composition Modifier .layout { measurable, constraints -> . .. . .. } .drawWithCache { . .. } ) } // perf tip #1: extract State read out of composition @shikasd_ @jossiwolf
  20. @Composable fun Example() { var state by remember { mutableStateOf(0)

    } Text( "$state" // read in composition Modifier .layout { measurable, constraints -> val size = IntSize(state, state) // read in layout . .. } .drawWithCache { . .. } ) } // perf tip #1: extract State read out of composition @shikasd_ @jossiwolf
  21. @Composable fun Example() { var state by remember { mutableStateOf(0)

    } Text( "$state" // read in composition Modifier .layout { measurable, constraints -> val size = IntSize(state, state) // read in layout } .drawWithCache { val color = state // read in draw } ) } // perf tip #1: extract State read out of composition
  22. @Composable fun App() { var scrollOffset by remember { mutableStateOf(0f)

    } ... Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = scrollOffset > 0f) Footer() } } // perf tip #3: derivedStateOf state read! @shikasd_ @jossiwolf
  23. @Composable fun App() { var scrollOffset by remember { mutableStateOf(0f)

    } ... val showScrollToTop by remember { derivedStateOf { scrollOffset > 0f } ) Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = showScrollToTop) Footer() } } // perf tip #3: derivedStateOf out of composition in composition @shikasd_ @jossiwolf
  24. @Composable fun App() { var scrollOffset by remember { mutableStateOf(0f)

    } ... val derivedScrollOffset by remember { derivedStateOf { scrollOffset - 10f } ) Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = derivedScrollOffset > 0f) Footer() } } // perf tip #3: derivedStateOf ❌ out of composition in composition @shikasd_ @jossiwolf
  25. @Composable fun App() { ... val showScrollToTop by remember {

    derivedStateOf { scrollOffset > 0f } ) val buttonHeight by remember { derivedStateOf { showScrollToTop ? 0f : 100f } } } // perf tip #3: derivedStateOf @shikasd_ @jossiwolf
  26. @Composable fun App() { ... val showScrollToTop by remember {

    derivedStateOf(structuralEqualityPolicy()) { scrollOffset > 0f } ) val buttonHeight by remember { derivedStateOf { showScrollToTop ? 0f : 100f } } } // perf tip #3: derivedStateOf @shikasd_ @jossiwolf
  27. @Composable fun App() { ... Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop =

    { scrollOffset > 0f } ) Footer() } val textOffset by animateValueAsState(scrollOffset) Text(Modifier.offset(textOffset)) } // perf tip #4: reduce scope of state update Recompose scope @shikasd_ @jossiwolf
  28. @Composable fun App() { ... Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop =

    { scrollOffset > 0f } ) Footer() } TextWithOffset(offset = scrollOffset) } @Composable fun TextWithOffset(offset: Int) { val textOffset by animateValueAsState(offset) Text(Modifier.offset(textOffset)) } // perf tip #4: reduce scope of state update Recompose scope @shikasd_ @jossiwolf
  29. @Composable fun Example() { var counter by remember { mutableStateOf(0)

    } Button(onClick = { counter ++ }) { Text("$counter") } } Recompose scope @shikasd_ @jossiwolf
  30. Frame #1 Frame #4 Recomposer:recompose Frame #2 Frame #3 PostColumn

    All Post composables get recomposed AppScreen @shikasd_ @jossiwolf
  31. // What makes a type stable? // 1. Immutability //

    2. Observable mutability (e.g. MutableState) @shikasd_ @jossiwolf
  32. @Composable fun Example($composer: Composer, ... ) { $composer.startRestartGroup(FunctionKey) val parametersChanged

    = /* change handling logic */ if (parametersChanged) { // function body } $composer.endRestartGroup() } @shikasd_ @jossiwolf
  33. Compose Compiler metrics // -classes.txt unstable class Model { stable

    val postType: String unstable var isSynchronized: Boolean <runtime stability> = Unstable } // -composables.txt restartable fun Post( unstable model: Model ) @shikasd_ @jossiwolf
  34. // -classes.txt unstable class Model { stable val postType: String

    unstable var isSynchronized: Boolean <runtime stability> = Unstable } // -composables.txt restartable fun Post( unstable model: Model ) class Model { val postType: String var isSynchronized: Boolean } @Composable fun Post( model: Model ) Compose Compiler metrics @shikasd_ @jossiwolf
  35. // -classes.txt stable class Model { stable val postType: String

    stable val isSynchronized: Boolean <runtime stability> = Stable } // -composables.txt restartable skippable fun Post( stable model: Model ) data class Model { val postType: String val isSynchronized: Boolean } @Composable fun Post( model: Model ) Compose Compiler metrics @shikasd_ @jossiwolf
  36. Frame #1 Frame #2 Frame #3 Only updated Post is

    executed Recomposer:recompose AppScreen PostColumn Frame #4 @shikasd_ @jossiwolf
  37. // -composables.txt restartable fun PostColumn( unstable models: List<Model> ) @Composable

    fun PostColumn( models: List<Model> ) @shikasd_ @jossiwolf
  38. Frame #1 Frame #2 Frame #3 Post Recomposer:recompose AppScreen PostColumn

    Frame #4 Post still has to be updated @shikasd_ @jossiwolf
  39. fun openPost(model: Model) { ... } @Composable fun PostColumn(posts: List<Model>)

    { Column { posts.forEach { model -> Post(model) { // onClick openPost(model) } } } } @shikasd_ @jossiwolf
  40. Post(model) { // onClick openPost(model) } // is compiled into:

    // file level class Post$1(private val model: Model) : Function0<Unit> { override fun invoke() { openPost(model) } } // inside composable Post(model, Post$1(model)) @shikasd_ @jossiwolf
  41. Post(model) { // onClick openPost(model) } // is compiled into:

    // file level class Post$1(private val model: Model) : Function0<Unit> { override fun invoke() { openPost(model) } } // inside composable Post(model, Post$1(model)) no .equals() implementation restart creates new instance @shikasd_ @jossiwolf
  42. Post(model) { // onClick println("Clicked!") } // is compiled into:

    // file level val lambdaParam = object : Function0<Unit> { override fun invoke() { println("Clicked!") } } // inside composable Post(model, lambdaParam) @shikasd_ @jossiwolf
  43. Post(model) { // onClick println("Clicked!") } // is compiled into:

    // file level val lambdaParam = object : Function0<Unit> { override fun invoke() { println("Clicked!") } } // inside composable Post(model, lambdaParam) no captures! @shikasd_ @jossiwolf
  44. Post(model) { // onClick println(model) } // is compiled into:

    // file level class Post$1(val model: Model) : Function0<Unit> { override fun invoke() { println(model) } } // inside composable val onClick = remember(model) { Post$1(model) } Post(model, onClick) @shikasd_ @jossiwolf
  45. Post(model) { // onClick println(model) } // is compiled into:

    // file level class Post$1(val model: Model) : Function0<Unit> { override fun invoke() { println(model) } } // inside composable val onClick = remember(model) { Post$1(model) } Post(model, onClick) stable capture! @shikasd_ @jossiwolf
  46. class PostActivity : Activity { fun openPost(model: Model) { ...

    } @Composable fun PostColumn(posts: List<Model>) { Column { posts.forEach { model -> Post(model) { // onClick openPost(model) } } } } } @shikasd_ @jossiwolf
  47. Post(model) { // onClick openPost(model) } // is compiled into:

    // file level class PostActivity$Post$1($this: PostActivity, model: Model) : Function0<Unit> { override fun invoke() { $this.openPost(model) } } // composable Post(model, PostActivity$Post$1(this, model)) stable! unstable! @shikasd_ @jossiwolf
  48. Defer reading state to a later phase (layout, drawing) Control

    how often state updates causes recomposition with derived state Check hot code paths for unstable types Measure if additional stability brings perf benefits State Reads Stability Compiler tries its best. 
 
 remember with care. Lambdas @shikasd_ @jossiwolf