State of Compose 2023 results are in! Click here to learn more
Published on

πŸš€ Blazing fast Composables

Authors

Happy Saturday! It's Alex πŸ‘‹

What was that? You thought there was not going to be a Composables issue this week? Composables are never going to give you up dear reader. Today we are covering a highly important topic that needed its sweet time to get just right.

This week we go through all about optimizing your composable functions. From understanding what might be causing your composables to be slow, optimizing our lists and tricks on how to make performant UIs in Jetpack Compose . It's all here.

Let's hit the pedal to the metal and let's go! 🏁


ELI5 – How to optimize your composable functions

We do not control how often or how many times our composable functions will be called. This means that having unnecessary calculations in our composable functions will be multiplied causing UI lag. Because of this, we need to keep unnecessary calculations to a minimum, or even better, outside our composable functions.

Jetpack Compose does tons of optimizations for you. It all works by assuming that the values rendered in your UI will change or not. Sometimes Compose cannot guess if a value is going to change or not. In those cases, we need to tell Compose manually what is going to change and what is not.

Lastly, composables go through 3 different phases before being rendered on the screen. Those are the Composition, Layout and Draw phases. We want to minimize the time it takes for each phase to run. Even better we want to skip phases entirely.

Always check performance on release builds

Jetpack Compose comes with a lot of optimizations out of the box. When you create a release version of your app, your app goes through all the optimizations, hence gaining significant improvements.

Because of this, it's best not to attempt to check for performance degradation on debug builds.

Keep expensive operations to a minimum with remember {} and derivedStateOf {}

Composables are Kotlin functions. We, as developers, do not control how often or how many times our composable functions will be called. Because of this, we need to make sure that any calculation is kept to a minimum. Otherwise, this cost will be multiplied.

In the majority of times, you want to keep any calculation happening in your ViewModel. Whenever a new state is calculated, the ViewModel emits it and the UI consumes it.

In cases where you have to do operations on your composables functions, make sure to wrap this calculation in a remember {} block. This will cause the calculation to be triggered only once and the value will be maintained across compositions.

val data = remember {
    // I/O is expensive, so only do it once
    readDataFromDisk()
}

Need to recalculate something when a value changes? remember {} has multiple overloads that accept multiple keys. When any of those key values change, the calculation runs again:

val data = remember(filePath) {
    // read data again if the given filePath changes
    readDataFromDisk(withId = filePath)
}

There is a catch though. remember {} might cause necessary UI updates if the value changes too many times. Whenever you have a stream of values coming in you might want to consider using derivedStateOf {} instead.

val state by viewModel.state.collectAsState()

val sorted by remember(state.listData) {
    derivedStateOf { state.listData.sorted() }
}

LazyList {
    items(sorted) { item ->
        Text(text = item)
    }
}

Optimize your LazyLists and LazyLayouts

When dealing with lists, you need to make sure that no extra calculations are done than needed.

If the items in your list can change (maybe through some user interaction), Jetpack Compose might try to re-render the entire list even if only one item was added (or removed), causing unnecessary compositions. You can fix this by giving each item of your list a unique key:

val contacts : List<Contact> = ...

LazyColumn {
    items(contacts, key = { it.contactId }) { contact ->
        Text(text = contact.displayName)
    }
}

Compose tends to reuse items that are no longer visible in the list. If you are rendering different layouts for different items, it is worth giving each item a different contentType. This way Compose will recycle different types more effectively.

LazyColumn {
    items(contacts,
        key = { it.contactId },
        contentType = { it.isFavorite }
    ) { contact ->
        Text(text = contact.displayName)
    }
}

PS: Keys are not limited to LazyLists. You can use the key {} function from any composable function if you need to tell Compose that this composable function can be identified by a specific id, such as in Column or Row.

@Stable vs @Immutable annotations

Jetpack Compose does tons of optimizations under the hood. One of those optimizations is based on the assumption that the values displayed on your composable functions is not going to change. Sometimes though, it is impossible for Compose to understand if a value is going to change or not.

Jetpack Compose comes with stability annotations to tell the compiler whether an object is going to mutate or not. In simple English:

  • @Immutable: The object's values are never going to change. An example of this are data classes with vals and no custom setters.
  • @Stable: The object's values might change. However, the developer is going to notify Compose when that happens (wrapping the value with remember { mutableStateOf() }).

It is worth going through the Compose Compiler Metrics and understand if your classes are marked as unstable.

Skip phases if possible

It is possible to skip phases that you do not need.

Compose offers multiple Modifiers that skip phases, such as Modifier.drawWithContent {} or Modifier.graphicsLayer {}, will only trigger a Draw phase, if the values they are holding changes.

There is currently no way to tell which ones skip phases and which ones don't, other than checking the documentation.

To my experience modifiers that accept lamdas might also skip phases. Double-check the docs to know for sure.

"Premature optimization is the root of all evil"

This phrase is a clichΓ© at this point, however it is still very true. Knowing where performance issues might take place in Jetpack Compose is important to be aware of. However, optimizing your composables without knowing if there is an actual problem is a problem itself.

Optimizing without having a proof that something is slow or not performance enough does not bring anything good on the table. It will not make the app better (as people will not notice the difference) and is probably going to make your code much more complex for no good reason.

Whatever you read here today is a tool like any other. Use it only when it makes sense to use it.


Optimization tools and further reading

Want to learn more? Here are the best blogs and resources on Jetpack Compose optimizations around the Net:

πŸ“š Resources & Videos

πŸ› οΈ Tools

  • Rebugger – A simple Compose library to print the reason for recomposition in your Logcat window.
  • Layout Inspector – Learn how to use Android Studio's Layout Inspector to find unneeded recompositions.
  • RecomposeHighlighter.kt on github.com

Ready made Jetpack Compose Components for your projects

Did you know there is now a Jetpack Compose Components page on Composables.com? It's where you can find components for most Jetpack Compose official libraries in one place, with photos and examples on how to use them.

Did you also know there are now ready-made Jetpack Compose Components you can use in your projects? UI components that are ready to be used in your apps, with no need to write any code.

Find all Pro components

New card based components

  1. Go to chrome://settings/searchEngines
  2. Add a new Site Search engine with the following values:
    • Search engine: Composables
    • Keyword: co (or whatever you prefer)
    • URL: https://composables.com/components/search/%s
  3. Save it
  4. Use 'co' in your search bar followed by the component's name
Search components


That is all about optimizations today folks. We saw what it means to optimize a Jetpack Compose app, how to use remember {} and derivedStateOf {} to keep expensive operations to a minimum, how to optimize your LazyLists and LazyLayouts, what are the @Stable, @Unstable and @Immutable annotations and how to skip phases if possible.

Keep your composables performant and see you again next week!