Improving NordVPN’s Android performance using the Compose Compiler plugin

Himanshu Singh

November 22, 2023


0

At NordVPN, we've harnessed the power of Jetpack Compose and its feature-rich capabilities to craft the user interface of our Android app. While Compose has accelerated our feature development workflow, our commitment to product quality drives us above all. We’re continuously exploring strategies for maintaining optimal performance.

android icon over blurry screen

The new Compose integration has sparked a lot of internal discussion regarding the app’s performance. One of the things we at NordVPN do is to integrate the Compose Compiler plugin in our day-to-day development process, which helps us improve our code. In this blog post, we explain what the Compose Compiler plugin is, how it works, and how we use it at NordVPN.

What problems did we have?

When working with Jetpack Compose, we encountered performance issues related to the stability of composable functions. Our primary goal in working with composable functions was to make them as stable as possible, or "skippable" in Compose terminology. In this context, "skippable" means that if a composable function is recomposed and its parameters haven't changed since the previous recomposition, Compose will skip the function and reuse the previous values.

With Compose, we can see significant performance improvements because even minor improvements can reduce recomposition count levels throughout the app. To assess the stability of composable functions, we can leverage the Compose Compiler plugin.

The Compose Compiler plugin

Compose Compiler is a plugin that can generate reports and metrics for components or code written in Compose. These reports provide detailed insights into the behavior of our Compose code. The plugin was added in version 1.2 of the Compose library.

With this detailed insight, we can begin making improvements to our code.

How does it work?

The Compose Compiler plugin is a Gradle task that generates reports for composable code within a module. It assesses the stability of the code, offering the flexibility to run it locally or in a CI pipeline when necessary.

It is recommended to generate the report in Release builds.

To ensure that the plugin works perfectly, we first need to configure it in the project's Gradle file.

1
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
2
compilerOptions {
3
if (project.findProperty("nordvpn-app.enableComposeCompilerReports") == "true") {
4
freeCompilerArgs.addAll([
5
"-P",
6
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
7
project.buildDir.absolutePath + "/compose_metrics"
8
])
9
freeCompilerArgs.addAll([
10
"-P",
11
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
12
project.buildDir.absolutePath + "/compose_metrics"
13
])
14
}
15
}
16
}

The first part of the code generates reports, while the second one generates metrics for those reports.

To run this code, we use the Gradle command as follows:

1
./gradlew assembleRelease -Pnordvpn-app.enableComposeCompilerReports=true

Generating a report on release builds is recommended.

When the execution is completed, it generates a file in the build folder like the one below.

compose_generated_report folder

compose_generated_report folder

Where,

*-classes.txt: contains information about classes referenced from a composable function.

*-composables.csv: CSV version of the TXT file

*-composables.txt: contains a detailed output of each Composable.

*-module.json: provides detailed statistics as a comprehensive view.

In our case, we’re primarily focused on the *-composables.txt files and will be working with those.

The image above displays the generated value for only one module. However, for NordVPN, we have multiple UI modules, and each module generates its own compose_metrics folder (that has its Compose code) with all relevant reports included.

Refining the generated report

With all of our modules generating reports, here’s an example of how an individual -composables.txt file can contain multiple blocks of code like:

1
restartable scheme("[androidx.compose.ui.UiComposable]") fun ScreenContent(
2
stable onBack: Function0<Unit>
3
stable onSettingToggled: Function0<Unit>
4
unstable state: State?
5
stable modifier: Modifier? = @static Companion
6
}

Each of these files contains numerous functions that exhibit a Kotlin-style code structure. Additionally, each module with Compose code has a dedicated text file. Before delving into the details, let's take a closer look at the significance of this code:

Restartable: When Compose detects changes in the function inputs, it restarts the function, invoking it again with the updated inputs.

Stable: This parameter in the provided function is stable; if they have not changed, Compose will skip it.

Unstable: This parameter in the provided function is unstable and Compose always recomposes it when the parent is recomposed.

We then merge all the *-composables.txt files into a single text file within our project using a script we've created for this purpose. This combined file plays a crucial role in our development process. Let’s see how we utilize it.

How do we use it in our day-to-day development?

At NordVPN, we've seamlessly integrated this workflow into our CI pipeline for every pull request we create, ensuring that we merge only stable Compose code (whenever possible) into our main branches.

However, before implementing this process, we conduct a thorough review to ensure that all of our Composable code contains no unnecessary unstable parameters. This proactive step guarantees that when we introduce this to our pull request flow, we initiate with a clean slate.

Let's take a closer look at the steps in our pull request workflow:

1. Create pull request: The process begins with the creation of a pull request (PR).
2. CI job: A Continuous integration (CI) job is triggered for the current PR if there’s a change in any of the UI modules. The CI job performs several tasks:
a. Generate report: We generate a report on the release branch, which results in the creation of multiple text files in each module containing Compose code.
b. Merge the text files: At this stage, we execute a script that combines these text files, retaining only the functions containing unstable parameters.
c. Create a markdown table: Next, we create a markdown table that lists the function names along with their associated unstable parameters.

Output Markdown table

Output Markdown table

d. Post comment: We post this markdown table as a comment within the PR. This informs developers about any potential instability introduced in the PR.
e. Fix: If instability issues are identified, we proceed to fix the affected functions and commit the changes.

The entire process is then rerun, and if the unstable parameter issues have been addressed, no further comment will be posted. Any previous comments on the matter can be resolved.

Execution of the CI pipeline

Execution of the CI pipeline

This practice aids us in utilizing the Compose Compiler plugin as a lint check for our Composable code, which maintains coding standards and contributes to improved performance for the NordVPN Android app.