Slide 1

Slide 1 text

Understanding Recomposition Performance Pitfalls Andrei Shikov (he/him) Compose @Google @shikasd_ Jossi Wolf (he/him) Compose @Google @jossiwolf compose

Slide 2

Slide 2 text

Understanding Performance

Slide 3

Slide 3 text

Improve Monitor Inspect @shikasd_ @jossiwolf

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Always test performance in release mode with R8 enabled — Ben Trengrove, twice! @shikasd_ @jossiwolf

Slide 6

Slide 6 text

Understanding Recomposition @shikasd_ @jossiwolf

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

State Recomposer Composition records read change observed by recomposes setContent { … } @shikasd_ @jossiwolf

Slide 9

Slide 9 text

State Recomposer Composition records read change observed by recomposes 1 2 3 @shikasd_ @jossiwolf

Slide 10

Slide 10 text

State Recomposer Composition records read change observed by recomposes @shikasd_ @jossiwolf

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

@Composable fun Example($composer: Composer, ... ) { $composer.startRestartGroup(FunctionKey) // function body $composer.endRestartGroup() ?. updateScope { $composer -> Example($composer, ... ) } } Recompose scope @shikasd_ @jossiwolf

Slide 13

Slide 13 text

@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

Slide 14

Slide 14 text

Recomposer change observed by State Composition records read recomposes @shikasd_ @jossiwolf

Slide 15

Slide 15 text

Recomposer change observed by State Composition records read recomposes @shikasd_ @jossiwolf

Slide 16

Slide 16 text

State change observed by recomposes Composition records read Recomposer @shikasd_ @jossiwolf

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

State change observed by recomposes Composition records read Recomposer @shikasd_ @jossiwolf

Slide 19

Slide 19 text

State recomposes Composition records read Recomposer modified in Snapshot observed by @shikasd_ @jossiwolf

Slide 20

Slide 20 text

• State is backed by Snapshot 📷 system • State changes in Snapshot are transactional and atomic (+ observers!) • Recomposer observes state changes through Snapshot system @shikasd_ @jossiwolf

Slide 21

Slide 21 text

State change observed by recomposes Composition records read Recomposer @shikasd_ @jossiwolf

Slide 22

Slide 22 text

Composition State change observed by recomposes records read Recomposer @shikasd_ @jossiwolf

Slide 23

Slide 23 text

Composition State change observed by recomposes records read Recomposer @shikasd_ @jossiwolf

Slide 24

Slide 24 text

@Composable fun Example($composer: Composer, ... ) { $composer.startRestartGroup(FunctionKey) // function body $composer.endRestartGroup() ?. updateScope { $composer -> Example($composer, ... ) } } Recompose scope @shikasd_ @jossiwolf

Slide 25

Slide 25 text

@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

Slide 26

Slide 26 text

State Recomposer Composition records read change observed by recomposes @shikasd_ @jossiwolf

Slide 27

Slide 27 text

Understanding Recomposition Performance @shikasd_ @jossiwolf

Slide 28

Slide 28 text

@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

Slide 29

Slide 29 text

@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

Slide 30

Slide 30 text

@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

Slide 31

Slide 31 text

Recomposer:recompose App Content Frame #1 Frame #2 measure Recomposer:recompose App Content measure Other @shikasd_ @jossiwolf

Slide 32

Slide 32 text

@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

Slide 33

Slide 33 text

// perf tip #1: defer State reads @shikasd_ @jossiwolf

Slide 34

Slide 34 text

@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

Slide 35

Slide 35 text

@Composable fun App() { var scrollOffset = remember { mutableStateOf(0f) } ... Column(Modifier.scrollable(scrollableState)) { Header() Content(showScrollToTop = { scrollOffset > 0f }) Footer() } } @Composable fun Content(showScrollToTop: State) { ... } // perf tip #2: defer reads ❌

Slide 36

Slide 36 text

/ / perf tip #2: extract State read out of composition @shikasd_ @jossiwolf

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

@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

Slide 39

Slide 39 text

@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

Slide 40

Slide 40 text

@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

Slide 41

Slide 41 text

Frame #1 measure measure Other Recomposer:recompose Frame #2 @shikasd_ @jossiwolf

Slide 42

Slide 42 text

// perf tip #3: Use derivedStateOf to reduce update frequency @shikasd_ @jossiwolf

Slide 43

Slide 43 text

@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

Slide 44

Slide 44 text

@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

Slide 45

Slide 45 text

@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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

@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

Slide 48

Slide 48 text

// perf tip #4: reduce scope of state update @shikasd_ @jossiwolf

Slide 49

Slide 49 text

@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

Slide 50

Slide 50 text

@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

Slide 51

Slide 51 text

@Composable fun Example() { var counter by remember { mutableStateOf(0) } Button(onClick = { counter ++ }) { Text("$counter") } } Recompose scope @shikasd_ @jossiwolf

Slide 52

Slide 52 text

Should you optimize all state reads? No. @shikasd_ @jossiwolf

Slide 53

Slide 53 text

@Composable fun PostColumn(posts: List) { Column { posts.forEach { model -> Post(model) } } } @shikasd_ @jossiwolf

Slide 54

Slide 54 text

Frame #1 Frame #2 Frame #3 @shikasd_ @jossiwolf

Slide 55

Slide 55 text

Frame #1 Frame #4 Recomposer:recompose Frame #2 Frame #3 PostColumn All Post composables get recomposed AppScreen @shikasd_ @jossiwolf

Slide 56

Slide 56 text

Understanding Stability @shikasd_ @jossiwolf

Slide 57

Slide 57 text

// What makes a type stable? // 1. Immutability // 2. Observable mutability (e.g. MutableState) @shikasd_ @jossiwolf

Slide 58

Slide 58 text

@Composable fun Example($composer: Composer, ... ) { $composer.startRestartGroup(FunctionKey) val parametersChanged = /* change handling logic */ if (parametersChanged) { // function body } $composer.endRestartGroup() } @shikasd_ @jossiwolf

Slide 59

Slide 59 text

// stability tip #1: immutable types or observable mutability @shikasd_ @jossiwolf

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

// -classes.txt unstable class Model { stable val postType: String unstable var isSynchronized: Boolean = 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

Slide 62

Slide 62 text

// -classes.txt stable class Model { stable val postType: String stable val isSynchronized: Boolean = 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

Slide 63

Slide 63 text

Frame #1 Frame #2 Frame #3 Only updated Post is executed Recomposer:recompose AppScreen PostColumn Frame #4 @shikasd_ @jossiwolf

Slide 64

Slide 64 text

@Immutable / @Stable @shikasd_ @jossiwolf

Slide 65

Slide 65 text

@Immutable / @Stable interface WindowInsetsController Children will be considered stable @shikasd_ @jossiwolf

Slide 66

Slide 66 text

// stability tip #2: not everything has to be stable @shikasd_ @jossiwolf

Slide 67

Slide 67 text

// -composables.txt restartable fun PostColumn( unstable models: List ) @Composable fun PostColumn( models: List ) @shikasd_ @jossiwolf

Slide 68

Slide 68 text

// -composables.txt restartable skippable fun PostColumn( stable contents: kotlinx.collections.immutable.ImmutableList ) @shikasd_ @jossiwolf

Slide 69

Slide 69 text

Frame #1 Frame #2 Frame #3 Post Recomposer:recompose AppScreen PostColumn Frame #4 Post still has to be updated @shikasd_ @jossiwolf

Slide 70

Slide 70 text

// stability tip #3: lambdas @shikasd_ @jossiwolf

Slide 71

Slide 71 text

Understanding Lambda Stability @shikasd_ @jossiwolf

Slide 72

Slide 72 text

// the haiku about // implementation details // fleeting in moment @shikasd_ @jossiwolf

Slide 73

Slide 73 text

Lambdas are always stable @shikasd_ @jossiwolf

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

class PostActivity : Activity { fun openPost(model: Model) { ... } @Composable fun PostColumn(posts: List) { Column { posts.forEach { model -> Post(model) { // onClick openPost(model) } } } } } @shikasd_ @jossiwolf

Slide 82

Slide 82 text

Post(model) { // onClick openPost(model) } // is compiled into: // file level class PostActivity$Post$1($this: PostActivity, model: Model) : Function0 { override fun invoke() { $this.openPost(model) } } // composable Post(model, PostActivity$Post$1(this, model)) stable! unstable! @shikasd_ @jossiwolf

Slide 83

Slide 83 text

Compiler isn't always smart, but it is consistent @shikasd_ @jossiwolf

Slide 84

Slide 84 text

// stability tip #3: lambdas - remember with care @shikasd_ @jossiwolf

Slide 85

Slide 85 text

That quote about premature optimization — Certainly somebody smart @shikasd_ @jossiwolf

Slide 86

Slide 86 text

Improve Monitor Inspect @shikasd_ @jossiwolf

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

xkcd.com/1691/ Andrei Shikov (he/him) Compose @Google @shikasd_ Jossi Wolf (he/him) Compose @Google @jossiwolf @shikasd_ @jossiwolf