Jetpack Compose aims to deliver great performance out of the box. This page shows how to write and configure your app for better performance and shows some patterns to avoid.
You might first want to familiarize yourself with the core Compose concepts in Thinking in Compose.
Properly configure your app
If your app is performing poorly, there might be a configuration problem. A good first step is to check the following configuration options.
Build in release mode and use R8
If you have performance issues, try running your app in release mode. Debug mode is useful for spotting many problems, but it imposes a significant performance cost and can make it hard to spot other code issues that might hurt performance. We recommend that you 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
Baseline Profiles improve code execution speed by about 30% from the first launch by avoiding interpretation and just-in-time (JIT) compilation steps for included code paths. By shipping a Baseline Profile in an app or library, Android Runtime (ART) can optimize included code paths through ahead-of-time (AOT) compilation, providing performance enhancements for every new app install and on every app update. This profile-guided optimization (PGO) lets apps optimize startup, reduce interaction jank, and improve overall runtime performance for end users from the first launch.
Compose is distributed as a library, instead of being part of the Android platform. This approach lets the Compose team 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, however, need to be loaded when the app launches and interpreted JIT when the functionality is needed. This can slow the app on startup and when 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 or AAB. During app installation, ART compiles this critical code AOT so that it's ready for use when the app launches.
It's not always straightforward 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, the Baseline Profile that ships with Compose only contains optimizations for the code within the Compose library. To get the best optimization, it's best to also create a Baseline Profile for your app that uses Macrobenchmark to cover critical user journeys. When you define your own profile you must 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 this 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 redraw this one element.
However, some coding mistakes can make it harder for Compose to know which phases it can safely skip. If it has any doubt, Compose runs all three phases, which can slow down your UI. So, many of the performance best practices are to help Compose skip the phases it doesn't need to do.
There are a couple of broad principles to follow that can improve performance in general:
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 gets re-executed, potentially for every frame of an animation. Limit the composable's code to only what it needs to build the UI.
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.