Improve your build times
Tags: gradle, android
Published: 2021-10-29 17:57:56

When I joined my current company a few months ago, the incremental builds of our Android app took an average of 14 minutes on my machine. You can imagine how wildly unproductive that made me feel, so I went on a journey to speed up our build times.

It's down to a little over 1 minute now, so I'll share some of the things I learned along the way. Some of these settings were already configured, and some weren't, but I'll share everything anyway, but please don't apply these changes blindly to your project and instead use the [gradle profiler](https://github.com/gradle/gradle-profiler) to ensure that they work for your existing setup.

## Easy wins
1. Use the latest versions of Gradle, JVM, and the plugins you use in your project.

    We were using AGP 4.2, and by updating to AGP 7, we were able to take advantage of the [perfromance improvmenets](https://docs.gradle.org/7.0.2/release-notes.html#performance-improvements) promised in Gradle v7. If you're stuck on AGP 4.2 for some reason, then enable [file-system watching](https://blog.gradle.org/introducing-file-system-watching) manually.

2. Enable parallel execution

    If you're working on a multi-module project, then forcing Gradle to execute tasks in parallel is also an easy performance gain. This works with the caveat that your tasks in different modules should be independent, and not access shared state, which you shouldn't be doing anyway. To enable it, add `org.gradle.caching=parallel=true` to your `gradle.properties` file.

3. Enable build caching

    This works by storing and reusing outputs produced by other builds if the inputs haven't changed. One feature of this is task output caching. It leverages Gradle's existing `UP_TO_DATE` checks, but instead of only reusing outputs from the most recent build on the same machine, it allows Gradle to reuse outputs from any earlier build in any location on the machine. When using a shared build cache for task output caching this even works across developer machines and build agents. To enable this, add `org.gradle.caching=caching=true` to your `gradle.properties` file.

## General advice

Before we move on to the next section, here's a quick primer on the Gradle build lifecycle. Every Gradle build has 3 phases:

* **Initialization**: this is where Gradle decides which modules are going to take part in the build, and creates a `Project` instance for each of them.
* **Configuration**: this is when the project objects are configured, and all the build scripts of all projects which are part of the build are executed. This phase is where you need to pay the most attention because it's executed with every build, so if you're firing off a network request here for some reason, please don't.
* **Execution**: here, Gradle executes the tasks that were created and configured during the configuration phase.

Now on to to some random bits of advice!

1. Really think about the plugins you add to your project

    Every plugin you add to your project adds time to the configuration phase, even if it doesn't do anything. So go through your plugins and remove the ones you're not using.

2. If you're using the Crashlytics or Firebase Performance Monitoring plugins, disable them for debug builds. 

    We saw _massive_ improvements by making these changes.

```
android {
    ...
    buildTypes {
        debug {
            ext.enableCrashlytics = false
            FirebasePerformance {
                instrumentationEnabled false
            }
        }
    }
}
```
You'll also want to disable Crashlytics at runtime for debug builds like this:
```
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
```

3. Convert build logic to static tasks

    If you have a lot of logic in your `build.gradle`, consider converting it to static Gradle tasks so Gradle can cache their results and alleviate their effects on your project's configuration phase time. For example, if you have code that determines the versionNumber based on the current Github branch, that's a good candidate to start with. While we're on this topic, always check and make sure your `build`gradle` files are as lean as possible. We removed a few legacy tasks/code snippets that didn't make sense in the scope of our project anymore this way.

4. Enable the parallel garbage collector if you're using JDK 9+

    You might want to profile this change first before enabling it for your project, but it can be enabled by appending the string `-XX:+UseParallelGC` to your `org.gradle.jvmargs=` in the `gradle.properties` file, or just by adding `org.gradle.jvmargs=-XX:+UseParallelGC` if you haven't customized these settings before.

5. Use the [gradle-doctor](https://github.com/runningcode/gradle-doctor) plugin

    I know I just said you should really think before adding plugins to your project, but this one's only purpose is to improve your builds by giving you warnings about issues it finds in your project, and you can remove it once you're done.

6. Enable non-transitive R classes

    Here's an excellent blog [post](https://blog.blundellapps.co.uk/speed-up-your-build-non-transitive-r-files/) about this by [Blundell](https://twitter.com/blundell_apps)

7. Enable configuration caching

    This is an experimental feature, so proceed with caution, but I can confirm that it was absolutely magical for us. Remember the configuration phase we talked about above? Well, this feature caches its results and reuses them for subsequent builds, similar to how build caching caches and reuses task outputs. You can enable it by adding the line below to your `gradle.properties` file, but please read about it [here](https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache)

    `org.gradle.unsafe.configuration-cache=true`

8. Disable the Jetifier

    Here's an excellent blog [post](https://adambennett.dev/2020/08/disabling-jetifier/) about this by [Adam Bennett](https://twitter.com/iateyourmic)