Jetpack Compose aims to deliver great performance out of the box. This page shows you how to write and configure your app for best performance, and points out some patterns to avoid.
Before you read this, you might want to familiarize yourself with the core Compose concepts in Thinking in Compose.
Properly configure your app
If your app is performing poorly, that might mean there's a configuration problem. A good first step is to check the following configuration options.
Build in release mode and use R8
If you're finding performance issues, make sure to try running your app in release mode. Debug mode is useful for spotting lots of problems, but it imposes a significant performance cost, and can make it hard to spot other code issues that might be hurting performance. You should also use the R8 compiler to remove unnecessary code from your app. By default, building in release mode automatically uses the R8 compiler.
Use a baseline profile
Compose is distributed as a library, instead of being part of the Android platform. This approach lets us update Compose frequently and support older Android versions. However, distributing Compose as a library imposes a cost. Android platform code is already compiled and installed on the device. Libraries, on the other hand, need to be loaded when the app launches, and interpreted just-in-time when the functionality is needed. This can slow the app on startup, and whenever it uses a library feature for the first time.
You can improve performance by defining baseline profiles. These profiles define classes and methods needed on critical user journeys, and are distributed with your app's APK. During app installation, ART compiles that critical code ahead-of-time, so it's ready for use when the app launches.
It's not always easy to define a good baseline profile, and because of this Compose ships with one by default. You might not have to do any work to see this benefit. However, if you choose to define your own profile, you might generate one that doesn't actually improve your app's performance. You should test the profile to verify that it's helping. A good way to do that is to write Macrobenchmark tests for your app, and check the test results as you write and revise your baseline profile. For an example of how to write Macrobenchmark tests for your Compose UI, see the Macrobenchmark Compose sample.
For a detailed breakdown of the effects of release mode, R8 and baseline profiles, see the blog post Why should you always test Compose performance in release?.
How the three Compose phases affect performance
As discussed in Jetpack Compose Phases, when Compose updates a frame, it goes through three phases:
- Composition: Compose determines what to show–it runs composable functions and builds the UI tree.
- Layout: Compose determines the size and placement of each element in the UI tree.
- Drawing: Compose actually renders the individual UI elements.
Compose can intelligently skip any of those phases if they aren't needed. For example, suppose a single graphic element swaps between two icons of the same size. Since that element isn't changing size, and no elements of the UI tree are being added or removed, Compose can skip over the composition and layout phases and just redraw that one element.
However, some coding mistakes can make it harder for Compose to know which phases it can safely skip. If there's any doubt, Compose ends up running all three phases, which can make your UI slower than it needs to be. So, many of the performance best practices revolve around helping Compose to skip the phases it doesn't need to do.
There are a couple of broad principles to follow that can improve performance in general.
First, whenever possible, move calculations out of your composable functions. Composable functions might need to be re-run whenever the UI changes; any code you put in the composable will get re-executed, potentially for every frame of an animation. So you should limit the composable's code to just what it actually needs to build the UI.
And second, defer state reads for as long as possible. By moving state reading to a child composable or a later phase, you can minimize recomposition or skip the composition phase entirely. You can do this by passing lambda functions instead of the state value for frequently changing state, and by preferring lambda-based modifiers when you pass in frequently-changing state. You can see an example of this technique in the Defer reads as long as possible section.