Make Gradle builds great (and fast) again!

Nobody likes to wait endlessly for their projects to build. Here are some methods that we at Tooploox employ to spend more time creating great apps, and less time building them.

Use the newest Gradle distribution

Gradle keeps getting faster and faster with each release, and it rarely makes sense to use older version. Android Studio keeps generating gradle wrapper that is ages behind - Android Studio 2.3 Canary 2 still uses version 2.14.1 - already few months old as of writing this article. Unless it breaks your build - and it shouldn't - update.

Project-wide

To update the gradle wrapper for your project, simply run wrapper task:

./gradlew wrapper --gradle-version <<version>> --distribution-type [all|bin]

for example

./gradlew wrapper --gradle-version 3.3 --distribution-type all

This will download specified gradle distribution into wrapper files. Also, consider periodically removing old versions leftovers from .gradle folder inside your project.

You can update gradle wrapper from Android Studio as well - open ProjectStructure dialog and update Gradle version field in Project tab. wrapper task is preferred, though, since it also updates the wrapper jar file as well as gradlew scripts.

System-wide

Despite gradle wrapper being the preferred method of running gradle builds, for non-production builds consider using locally installed gradle distribution. This way you can benefit from the latest performance improvements while not having to modify (possibly someone else's) project's gradle wrapper. To install gradle locally, refer to official website, or use packet manager like Macports or Homebrew. And update it periodically!

Configure Gradle properly

Gradle is extremely complex build system and as such it can be configured extensively. We'll cover only some options - the ones that are the most important from the performance standpoint.

Project-wide

Project-wide settings for your gradle build are kept in gradle.properties file in your project's root folder.

System-wide

System wide settings can be found in .gradle folder in your HOME directory - ~/.gradle/gradle.properties. On Windows look in C:\Users\username\.gradle\gradle.properties).

First Gradle reads all project-wide options, followed by system-wide options. If an option is specified twice, the last one wins. This essentially means you always override project's properties by your system-wide options.

Options

  • org.gradle.daemon - should be set to true to take advantage of Gradle daemon. Long story short, daemon means reusing things in subsequent builds
  • org.gradle.jvmargs - specifies jvmargs used for daemon process. You want to set high enough memory limit to fix dex in Gradle process, so minimum configuration here is -Xmx2g, while 3-4 gigabytes won't hurt either
  • org.gradle.configureondemand - slight optimisation that, if set to true, configures only needed modules of the project. Useful in large, multi-module projects
  • org.gradle.parallel - allows Gradle to build modules in parallel. Only useful, if your modules don't depend on each other

Sample, efficient gradle.properties file for a 16GB machine looks like this:

org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

Remember that -Xmx argument should be set to at least your dexOptions.javaMaxHeapSize option plus some memory for Gradle build itself. If you don't specify dex heap size manually, 1GB is used by default.

Convert your pngs to webps

Android has partial (no transparency and no lossless encoding) support for webp format since Android 4.0 (API 14), and full support came with API 18. If you're lucky enough to be able to use it, Android Studio 2.3 offers quick fix that converts your existing pngs into webps - simply right-click image (or a folder, even res) and choose Convert to WebP.... This can shave off several megabytes, from your APK, which may also speed up sending your application to the device.
One thing to keep in mind is that webp images aren't supported as launcher icons!

Edited: processing or merging resources is not, in fact, related to dexing. Previously stated significant speedup from using webp instead of png was the result of aapt not crunching images anymore. Thanks to Jake Wharton for pointing this out!

Do not crunch your pngs

If you're not lucky enough to use webps, you can still speed up your build by disabling png crunching. From Android documentation on drawables:

Image resources placed in res/drawable/ may be automatically optimized with lossless image compression by the aapt tool during the build process. For example, a true-color PNG that does not require more than 256 colors may be converted to an 8-bit PNG with a color palette.

This, obviously, takes time. You can skip this optimisation by providing proper option to aapt:

android {
    ...
    aaptOptions {
        cruncherEnabled = false
    }
    ...

In one of ours png-heavy projects, this alone reduced build times from 4 minutes to 40 seconds. You may want to reenable this for production build (or disable only for development) though, as crunching is, after all, an optimisation.

Use dex in process

Fortunately this is now enabled by default, but to reiterate - make sure your gradle daemon has enough memory allocated (at least 1GB)!

If at some point your build suddenly slows down, it might be the sign that your heap size is not big enough to fit dex in process anymore. You can then increase dex memory size in your module's build.gradle file:

dexOptions {
    javaMaxHeapSize "2g"
}

Remember that afterwards you need to increase heap size for Gradle as well!

Increasing java max heap size for dex process should be used sparingly, as it's rarely needed.

Pre-dex libraries

Simply setting preDexLibraries to true may speed up incremental builds.

dexOptions {
    preDexLibraries true
}

It can slow down clean builds though, so it's worth disabling this option for builds in your CI environment.

Set minSdkVersion to 21+ when using Multidex

If you use multidex, you can greatly improve your build times if your minSdkVersion is set to 21+. You can either create a special dev flavor for you app that overrides minSdkVersion, or have the version computed dynamically based on a gradle property:

android {  
  defaultConfig {
    minSdkVersion hasProperty('minSdk') ? minSdk.toInteger() : 16
  }
}

And then compile with, for example, ./gradlew installDebug -PminSdk=23. You can set -PminSdk=23 option in Build->Compiler->Command line options in your Android Studio as well, to use this option for builds triggered from your IDE.

Remember that Android Studio uses specified command line options also for generating signed APK. If you're preparing a release APK, you might want to use command line, to be sure you haven't built yours with too high minimum SDK version.

Avoid computations in Gradle configuration phase

When you trigger a build, at first your gradle projects are configured. This (more or less) runs the parts of your gradle build scripts that aren't tasks. There's a popular trick of having git sha in your BuildConfig class:

def getGitHash = { ->
    def stdout = new ByteArrayOutputStream()
    exec {
        commandLine 'git', 'rev-parse', '--short', 'HEAD'
        standardOutput = stdout
    }
    return stdout.toString().trim()
}
android {
    ...
    buildTypes {
        debug {
            buildConfigField "String", "GitHash", "\"${getGitHash()}\""
        }
    }
    ...
}

The problem here is that every time you run a task, getGitHash will be run. While this particular action may not be very inefficient in itself, lots of such small actions pile up. Consider only running these when building certain flavours, like staging builds, or only on CI environment.

This is also important when applying external plugins. You most likely use Crashlytics in your app - for development purposes it is a good idea to disable not only crashes reporting (directly in your application), but also Fabric plugin itself:

buildTypes {
    release {
        ...
    }
    debug {
        ext.enableCrashlytics = false
    }
}

Limit packaged resources configurations

In projects with multiple configurations (like translations) you can save some time by only packaging resources for single language or screen size:

android {
    productFlavors {
        dev {
            resConfigs "en"
    ...

You can mix configurations, so resConfigs "en", "xhdpi" would be also valid, as well as for example resConfigs "en", "fr", "xxhdpi".

Use JCenter

Long story short, there is now no reason to use mavenCentral() over jcenter() in your repositories blocks. JCenter is bigger, more secure and - even if ever so slightly - faster.

Use specific dependencies versions

Aside from being bad practice, using + in dependencies versions forces gradle to check for newest dependency version each time you build your project. Always use specific library version, like compile 'com.squareup.retrofit2:retrofit:1.9.0' instead of compile 'com.squareup.retrofit2:retrofit:1.9.+'

Use instant run (sometimes)

Instant run speeds up builds immensely - if it works. Keep an eye on weird bugs or inexplicable errors. In bigger projects (or for some other reason less-instant-run-friendly projects) you might be better off spending more time building, and less time debugging non-existent errors.

Update Java

Simply put, Gradle is written in Java, so it'll benefit from performance improvements in newer JVM implementations. While Android Studio now bundles its own JDK, if you still use older Java on your machine, updating it will also speed up Gradle outside Android Studio.

Profile

Above all, you should always profile and see where there's place for improvement. Doing this for Gradle build is as simple as providing --profile option in the command line:

gradle app:assembleDebug --profile

In your build/reports/profile directory, you'll find an HTML report with summary of times and details for both configuration phase and task execution. This will give you some insight into which parts of your build deserve attention.

That's it. Let us know what helped you most, and what other tricks you use to speed up your builds!