Jetpack Compose is designed to be interoperable with an existing View-based app. This enables you to take an incremental approach to migrating your existing app’s UI to Compose.
While your app is in the middle of migration, and doesn’t yet have a Composable on the home or landing screen, users might notice jank when they navigate to a screen built with Compose. This is because the Compose library is loaded only when it is used for the first time. There is one workaround that has been floating around to add a Composable pixel on the home/landing screen. This workaround ensures that Compose components are warmed up before they are used. We don’t recommend taking this approach since enforcing it in your app results in a code that doesn’t contribute to your feature implementation and may lead to unforeseen bugs. Rather, we recommend a combination of the App Startup library and custom baseline profiles to solve the jank issue. We will benchmark and visualize how the combination can help to improve the app’s performance.
Using Baseline Profiles
Baseline Profiles help improve app startup and runtime performance of Android applications. Baseline Profiles are a list of classes and methods included in an APK. They are used by Android Runtime (ART) during app installation to pre-compile critical paths into machine code. This is a form of profile-guided optimization (PGO) that lets apps optimize startup, reduce jank, and improve performance for end users by reducing the amount of code that has to be Just-In-Time (JIT) compiled.
Jetpack Compose is distributed as a library. This enables frequent updates to Compose and backward compatibility to older Android versions. But distributing as an unbundled library comes with a cost; libraries need to be loaded when the app launches, and interpreted just-in-time when the functionality is needed (for more details checkout Why should you always test Compose performance in release?). This can lead to longer app startup time or jank whenever the app uses a library feature for the first time. To solve this issue, Compose provides a Baseline Profile to ahead-of-time compile Compose when the app is installed. In most cases, the default Baseline Profile provided by Compose is enough to deliver great performance. But customizing the profile based on your app’s common user interactions often produces better results.
To analyze this performance improvement, we wrote some Macrobenchmark tests and measured the performance on the Sunflower sample app. The app is partially migrated to Compose and doesn’t have any Composable on the landing/home screen.
For this performance analysis, we will be interacting with three screens from the app:
- The Home screen that is built with View based design and doesn’t have any Composables in it.
- The Plant List screen is built with RecyclerView and each item inside the view is built with Compose. It uses the ComposeView View to host the Compose content.
- The Plant details screen that is built with Compose and uses AndroidViewBinding to create Android layout resources and interact with it.
The Sunflower app code has a BaselineProfileGenerator class that creates a baseline profile. We executed the test and created a custom baseline profile focused on these critical user journeys (CUJs). This profile is available inside the baseline-prof.txt file under the main source-set. Compared to the default baseline profile provided by Compose, the custom baseline profile does show performance improvements. Let’s take a look at the test results for a couple of scenarios:
- [Scenario 1]From the Home screen, the user navigates to the Plant List screen that is partially built in Compose. The benchmark test is available in the PlantListBenchmarks class.
Macrobenchmark runs your test multiple times and outputs the results as a statistical distribution. The distribution is represented in percentile P50 — P99. Following were the results when we ran the benchmark tests:
For the data shown in the table above, P50 for CompilationMode.None means, 50% of frames were rendered faster than 7.1 milliseconds.
- [Scenario 2] User navigates to a screen fully built with Compose and uses AndroidViewBinding to interact with TextView. The benchmark test is available in the PlantDetailBenchmarks class. Following were the results when we ran the benchmark tests:
Note: The benchmark tests were executed on Samsung Galaxy Fold, your benchmark results may show different numbers but overall performance impact should be similar.
When you take a close look at P99 numbers, you will notice significant improvements. These outliers are usually the ones that cause jank.
To further improve app performance we can use the App Startup library to warm up the Compose components before they are actually used. Let’s give it a try and see what we discover.
Warm up Compose library with App Startup library
The App Startup library provides a structured way to initialize components at application startup. Using the App Startup library, you can warm up the Compose library during app startup.
Follow these steps to add the Compose initializer using App Startup library:
- To use Jetpack startup in your app, add the dependency on startup-runtime in your app’s build.gradle file.
To warm up the Compose component, create a new
ComposeInitializer class as shown in the following code snippet.
Initializing the ComposeView this way won’t add the content to the view hierarchy, but it still helps to initialize the Compose component.
- To make the ComposeInitializer discoverable by App Startup, navigate to AndroidManifest.xml file in your project. Add a
InitializationProvidermanifest entry as shown in the code below:
InitializationProvider is a special content provider that helps to discover and call the component initializer you have just defined.
- Re-generate and update the baseline profile to take into account the changes to startup. The baseline profile generator is part of macrobenchmark module. You just need to execute the
- Re-run the macrobenchmark tests from the
Here’s what we discovered for each of the previous scenarios:
- [Scenario 1]From HomeScreen, the user navigates to the Plant list screen.
When you take a close look, for the screen that is partially built with Compose, the outliers seem to have an improvement of ~21%. It is often the outlier frames that cause jank.
- [Scenario 2] User navigates to the Plant Details screen.
There is ~27% percent improvement in the outliers.
In both the cases, adding App Startup initializer improved the performance, especially for the outliers in P99.
Here is the link to access the code on GitHub.
Now the question is, should you always add the App Startup library for such scenarios, and is it applicable for all of your projects? Well, before you make such a decision, we strongly encourage you to:
- Write benchmark tests and measure the app’s performance.
- Implement the changes
- Re-run your tests to verify if performance is really improved.
This approach to implementation would help you in the longer run to evaluate if the suggestions are really applicable for your app. Let the statistical data drive your implementation choices for improving performance.
- Always add a Baseline Profile, regardless of needing the App Startup library.
- The combination of custom baseline profile and App Startup library leads to better performance results when your app doesn’t have any Composables on the home/landing screen, and the user navigates to a screen which is either partially or completely built with Compose.
- Instead of adding a Composable on home/landing screen for sake of performance (and not because of design choice) you can use the App Startup library to warm up Compose components.
- Just because someone suggests doesn’t mean you have to follow that advice. Write Macrobenchmark tests to check if App Startup library really improves your app’s performance. Let the statistical data drive your implementation choices to ensure they result in performance improvement.
Thank you Ben Trengrove, Florina Muntenescu, Ben Weiss, Ryan Mentley, and Rahul Ravikumar for helping with the reviews.