Optimizing Android Build Time

Android developers get all the benefits of a nice statically compiled language, but that comes at a cost – build time. Compile times can add up quickly and be a big disruption in workflow. Builds taking minutes are not uncommon. Below are steps to decrease that time.

Assess

How long should an Android app take to build? There are a lot of variability here: complexity of project, dependencies, annotation processors, versions of gradle & build tools, build machine CPU/memory. There is also clean build vs incremental build.

So what is a ‘good’ incremental build time? I’d say 10 – 15 seconds is a good, achievable goal for many apps.

Build Scan

Build Scan is a service provided for free by Gradle that gives insight into your build. Run your build with the --scan flag and you will be given a link at the end of the build to a site with data on your build. The report has a break down of each task, visualization of parallelism, build cache hits/misses, and more. BTW, these reports can be deleted easily.

./gradlew assembleDebug --scan

Profile Report

Another tool in the Gradle toolbox is Profile Report. This gives you a break down of how long each task took, but keeps it local. Not as detailed or actionable as Build Scan, but a good option for the paranoid among us.

./gradlew --profile assembleDebug

Optimize gradle.properties

Several options in your gradle.properties file can speed things up:

#Use a background Gradle Daemon to run the build.
org.gradle.daemon=true

#Give it some decent memory
org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

#Incremental builds on annotation processing
kapt.incremental.apt=true

#run tasks in parallel when possible
org.gradle.parallel=true

#Gradle will attempt to only configure neccesary projects
org.gradle.configureondemand=true

#Gradle will reuse task outputs from any previous build,
# when possible, resulting is much faster builds
org.gradle.caching=true

Modularize

Long gone are the days of Gradle not being able to handle multiple module projects. Now-a-days it is best practice to break your project up in to many modules. There are a number of benefits including: clear lines of seperation, code sharing between projects, dynamic app updating, and preparing for Instant Apps.

On our topic, modularization allows Gradle to do parallel tasks. This can speed up builds tremendously if done properly. Keep in mind mileage will vary depending on your project’s dependency graph.

Dev Build Flavor

Creating a build flavor just for development allows you to cut out some work that you don’t need. Consider removing:

  • crashlytics plugin
  • resources not needed (other languages, res folders not needed for test devices)
  • any dynamic data from BuildConfig
 buildTypes {
        debug {
            applicationIdSuffix ".debug"
            minifyEnabled false
            buildConfigField 'String', 'BUILD_TIME', "\"NOT_REPORTED\""
            //if not reporting crashlytics disable mapping file upload
            firebaseCrashlytics {
                mappingFileUploadEnabled = false
           }
        }

Keep your BuildConfig.java static between builds of your development flavor. This will leverage your build cache more effectively. A common pitfall here is to put the build time as a buildConfigField. This is fine, but in your development flavor set it to a constant value.

    flavorDimensions "package"
    productFlavors {
        dev {
            dimension "package"
            resConfigs "en", "xxhdpi"
            splits.abi.enable = false
            splits.density.enable = false
            aaptOptions.cruncherEnabled = false
        }
        normal {
            dimension "package"
        }
    }

Using resConfigs "en", "xxhdpi" will exclude other languages and other resource buckets from the apk. This can save alot of time if you have a lot of resources.

That’s it for now!

Software Engineer, Husband, and father of 2 amazing kids. Android, iOS, Kotlin multiplatform!! Maintainer of ReduxKotlin.org