DEV Community

Adam
Adam

Posted on

Improve your build times

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 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 promised in Gradle v7. If you're stuck on AGP 4.2 for some reason, then enable 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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You'll also want to disable Crashlytics at runtime for debug builds like this:

FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
Enter fullscreen mode Exit fullscreen mode
  1. 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 buildgradle` 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.

  2. 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.

  3. Use the 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.

  4. Enable non-transitive R classes

    Here's an excellent blog post about this by Blundell

  5. 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

    org.gradle.unsafe.configuration-cache=true

  6. Disable the Jetifier

    Here's an excellent blog post about this by Adam Bennett

Top comments (0)