Performance best practices for Jetpack Compose (GoogleIO 2022 recap)
Configuration
First of all, don’t forget to turn on R8 in the release mode for the performance measurement, and let’s get to the point.
Gotchas
something to remember
Let’s see what is a problem in below.
The problem is that sort runs whenever recompositions occurs in LazyColumn. This kind of expensive operation should be in remember function. In below, sort went inside remember(), and contact list and comparator were passed to remember(). This prevents sort to run whenever recompositions and makes sort run only when contacts or comparator is changed.
LazyList Key: Define a key on your LazyList items
Compose uses the position of each item as a key if key is not defined, which means when you move an item recompositions occur for every items under the item you moved.
You can simply pass a function to key parameter to define the key like below.
derivedStateOf {}: Use to buffer the rate of change
The example below shows a button to scroll to top when scrolling list to down. It passes firstVisibleItemIndex>0 to the parameter of AnimatedVisibility.
This is very simple example but we get an issue. LasyList updates listState for every scrolling frame! Unnecessary recompositions occur because you read listState.
derivedStateOf can limit recompositions only when the value is changed like below. It is the good case to change much stream to boolean values.
However derivedStateOf is not for every case. In below, you need the count of items of contacts list and the count should be updated whenever items are changed. That means derivedStateOf is not necessarily.
Reading state: Defer reading state until you need it
Let’s make an animation to change the background from cyan to magenta. It is very easy for Compose like below. However you have performance issues which is difficult to spot, which means Compose is doing much more work than needed.
You need to understand how Compose works for this problem. Compose has three phases-Composition, Layout, and Draw like below. These are repeated in every frame where data is changed.
Then, you can skip some phase if data is not changed. In the above example, recomposition occurs in every frame because of the color animation. It would be better to skip composition and layout phases if only color is changed.
It is an important concept to defer reading state until you need it. It makes functions to be executed again less frequently and skip Composition or Layout.
Below example used drawBehind() instead of background(). drawBehind() needs a function to execute in Draw phase. Then Compose executes only Draw phase without Composition and Layout phases when the color is changed.
This concept is not applied only for the special cases like animation. Below example shows re-executing is limited only in Text() when contact.name is changed. ContactCard or MyCard is not re-executed because they do not read contact.name.
Backwards write: Writing to state you have already read
Do you know which problem the below example has?
After running the above example, you can see recompositions are occurring in every frame when you runs system trace in CPU view of profiler from Android Studio.
It is the problem to change balance. Compose has the core assumption that the value that Compose already read is not changed until Compose ends. Rewriting to the value already read is called Backwards write.
remember() can solve this problem like below. Of course you can calculate it in ViewModel like the previous sort problem.
Baseline Profiles: Speed up startup and hot paths
This section is about speed up with AOT compile. It is also important but I don’t think I need to summarize it. I skip it. Please refer to links below for the details.