Comparing Jetpack Compose performance with XML

Rajan Gupta
7 min readMay 18, 2024

As Jetpack Compose is progressing towards stability, our team decided to measure the performance differences between Compose and XML-based layouts to build the foundation for gradually moving towards Jetpack Compose. The differences were measured in terms of frozen frames, slow frames, and page load duration. Additionally, we deep-dived into how Android Runtime (ART) optimizes the performance by using Ahead-of-time (AOT) compilation.

Both the below-mentioned experiments were performed on a Production build with R8 enabled. The compose version used throughout was 1.1.0

Let’s get started…

Experiment — 1

To begin with, we decided to convert one of the existing screens from XML to compose. Then, using our internal A/B experimentation platform, we made 2 equal cohorts/groups of users. The first cohort would experience the old XML-based screen whereas the other cohort would experience the newly compose built screen.

Aim — To understand the rendering performance of both the screens by comparing frozen frame rate, slow frame rate, and page load duration.

Screen — This screen shows all the transactions as well as their summary in an interval.

Results -

Frozen frame percentage:

For the compose-based layout, the frozen frame rate came out to be 0.044% whereas it was 0.034% for the XML-based layout. An alternate way to understand this can be, that if we open this XML-based activity, then nearly 3.4 frames out of 10,000 frames would fall under the frozen frame category. Similarly, for the compose-based activity, it would be 4.3 frames out of 10,000 frames.

Slow frame percentage:

For compose-based layout, the slow frame rate came out to be 8.91% whereas it was 9.93% for XML-based layout. Like frozen frames, ~10 out of 100 frames would fall under the slow frame category for XML-based layout. Whereas it was ~9 out of 100 frames for compose-based activity.

Median Page Load Duration (in milliseconds):

Internally, we define page load duration as the time taken from launching an activity to drawing its first frame. Compose-based layout nearly took 2.5x the time than the XML-based layout.

Understanding the reason behind these metrics

In order to understand insights, we deep-dived into how Android Runtime (ART) works under the hood. Before jumping onto it, let’s get familiar with some basic terminologies:

Just-in-time (JIT) — Each time when we run the app, the compiler interprets the code on the fly and converts the dex bytecode into machine code.

Ahead-of-time (AOT) — During the app installation phase, the compiler statically translates the dex bytecode to machine code and stores it in the device’s storage to avoid overheads while the app is being used.

More details can be found here.

Key understandings from the first experiment -

Internally, ART uses a combination of JIT and AOT to interpret dex bytecode. Also, it is our intuition that most of the components used for XML-based layouts are compiled ahead-of-time by the compiler whereas compose components, just like our code, are compiled just in time. Since the compose components are unbundled on the fly, it might be taking a significant amount of time to render the layout. To validate the idea, we moved toward the next experiment.

You can understand more about how ART works from here. Let’s understand what we tried in the next experiment…

Experiment — 2

In order to measure the change in metrics, we converted the screen just previous to the above experiment’s screen to compose. So, the group of users, who experienced the compose-based layout under the first experiment, will again experience the compose-based layout for the previous screen. Similarly, the other group would experience XML-based layouts.

Aim — To analyse the performance differences of the first experiment’s screen once some compose components are pre-compiled by the compiler.

Screen — The screen, previous to the first experiment’s screen, gives a quick summary of customer and supplier balances.

Results -

Note: We measured frozen frame percentage, slow frame percentage, and page load duration for the same screen which was under the first experiment’s observation.

Frozen frame percentage:

Slow frame percentage:

Median Page Load Duration (in milliseconds):

Key Takeaways -

On comparing the results with the previous experiment, we figured out a few really interesting insights. Let’s go over them one by one:

  1. The frozen frame rate of the screen, under observation, dropped drastically once its previous screen loaded compose components for it.
  2. We saw an improvement from ~60% for the frozen frame rate. (Both are similar) Also, the compose layout performed ~4% better in terms of slow rendering.
  3. Page load duration improved by ~35% as the compiler had unbundled major compose components for rendering.

Based on the results of this experiment, we can confidently say that our intuition at the end of the first experiment was in the right direction. Compose components are interpreted just-in-time during the initial rendering but internally, ART understands the function logs and pre-compiles the classes whenever we need them next.

Final thoughts

Let’s briefly go over the latest metrics to conclude our discussion.

After preloading compose we saw a 70% reduction in frozen frames and a 35% reduction in median page load duration. Currently, we have rolled out the compose screens to all the users and we expect the metrics to improve further.

To conclude our discussion, we were able to prove our hypothesis of ART, which uses a combination of JIT and AOT, was able to efficiently load the screen, under observation, once the composed components are pre-compiled. Components used by XML layouts are always pre-compiled and hence, bound to perform well. But on the positive side, Jetpack compose has its own benefits of improving the performance as more and more components are interpreted ahead of time.

Not only focusing on the performance gains, Jetpack Composes has also improved the dev acceleration by providing modular and intuitive declarative APIs. With time, we got more familiar with compose and understood how it helps in effectively creating complex yet modular UI components. We, at OkCredit, definitely recommend you use Jetpack Compose.

We hope that our experience will be valuable in helping you to understand the performance difference between XML and jetpack compose. If you have any further questions regarding the experiment, please do not hesitate to ask in the comment section. Thanks for reading.

Bonus section

We are measuring per-frame durations using the FrameMetricsAggregator API, we measure each frame’s duration from OnStart to OnStop for each fragment and count total frames, total frozen frames (700ms+), and total slow frames (16ms+).

import androidx.core.app.FrameMetricsAggregator
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class FragmentFrameRateTracer constructor(
private val activity: FragmentActivity,
private val label: String = "Screen"
) : DefaultLifecycleObserver {

private val aggregator: FrameMetricsAggregator = FrameMetricsAggregator()

private var totalFrames = 0L
private var slowFrames = 0L
private var frozenFrames = 0L

override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
activity.lifecycleScope.launch(Dispatchers.Default) {
aggregator.add(activity)
}
}

override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
activity.lifecycleScope.launch(Dispatchers.Default) {
val data = aggregator.metrics ?: return@launch

totalFrames = 0L
slowFrames = 0L
frozenFrames = 0L

data[FrameMetricsAggregator.TOTAL_INDEX].let { distributions ->
for (i in 0 until distributions.size()) {
val duration = distributions.keyAt(i)
val frameCount = distributions.valueAt(i)
totalFrames += frameCount
if (duration > 16)
slowFrames += frameCount
if (duration > 700)
frozenFrames += frameCount
}
}
aggregator.reset()
val frameRateData = FrameRateData(totalFrames, slowFrames, frozenFrames)

// Record your analytics events here
// You can use ${frameRateData.getSlowFrameRate()}, ${frameRateData.getFrozenFrameRate()}
}
}
}

What do you think? Let me know in the comments.

“Thank you for taking the time to read this article. If you found it helpful, I’d greatly appreciate your support by giving it a like or sharing it with fellow developers who might benefit from it. Your engagement motivates me to create more content like this. Additionally, if you have any suggestions or would like to connect, feel free to reach out to me on my LinkedIn profile. Let’s continue to learn and grow together in the world of Android development!”

https://www.linkedin.com/in/rajan-r-gupta

References -

  1. https://engineering.premise.com/measuring-render-performance-with-jetpack-compose-c0bf5814933

--

--