How To Speed Up Android Builds

Slower project build times may result in lower productivity. Lower productivity is lost money for the business. In this article, I provided a list of configurations, and tips you can implement for speeding up your Android project's Gradle builds.

How To Speed Up Android Builds
Photo by Guido Coppa / Unsplash

Introduction

Slower project build times may result in lower productivity. Lower productivity is lost money for the business. In this article, I provided a list of configurations and tips you can implement for speeding up your Android project's Gradle build.

The steps I outlined before improved our project's overall Android build speeds by up to 30% at Freelancer.com.

Profile your build

Numbers don't lie. Profiling your build in Android will provide insight into your build times. Profiling should help you better understand how long Gradle takes to run your project's tasks.

To profile your builds, you can run the following:

./gradlew assembleDebug --profile.

Also, you can add it to the command-line options in the Android Studio Preferences:

Command Line Options --profile

Use latest Gradle

The Gradle team releases newer versions of Gradle with faster compilation and build times. At the time of writing, the latest release is Gradle 5.4.1.

The newer version changes may introduce unwanted compilation errors or compatibility issues. Check out and follow the migration guide when upgrading your Gradle.

Gradle 5.0


Image from https://gradle.org/whats-new/gradle-5/

Estimates that this will reduce 20-25% of your build time.

More on Gradle 5 changes here.

Avoid Legacy Multidex

Android build architecture has a limitation of 65,536 method references, a.k.a. the 64K limit. Once a single Dalvik Executable or DEX file reaches the limit, you will encounter a build error. Multidex was introduced to help you avoid the 64K limit.

To support multidex, add and set multiDexEnabled to true.

android {
    defaultConfig {
        ...
        minSdkVersion 21
        targetSdkVersion 28
        multiDexEnabled true
    }
}

If your minSdkVersion If set to 20 or lower, you must add the support library.

android {
    defaultConfig {
        ...
        minSdkVersion 15
        targetSdkVersion 28
        multiDexEnabled true
    }

    dependencies {
        implementation 'com.android.support:multidex:1.0.3'
    }
}

However, this project setup will make your builds run with legacy multidex.

Legacy Multidex

Legacy multidex happens when you build your project with multidex enabled and minSdkVersion is set to 20 or lower. Clean and incremental builds are significantly slower on this.

The simplest way to solve this is by creating a build variant for development. With this, you can run an isolated build variant on your test device without worrying about legacy multidex.

android {
    defaultConfig {...}
    buildTypes {...}
    sourceSets {...}
    productFlavors {
        // Create a separate build variant for development which has a minSdkVersion of 21
        dev {
            ...
            minSdkVersion 21
        }

        prod {...}
    }
}

Estimates that this will reduce 5-10% of your build time.

Disable lint checks on development

I recommend this for local development only, not for your continuous integration pipeline.

If lint checks, especially if your project is large, take up at least 30% of your time every build; think again. You may only want to run your lint checks when creating a diff, such as running a script with lint checks specified as one of the tasks to execute before diff creation.

Option 1. Disabling lint check on gradle.properties. Not recommended when running builds in your CI, as you may want to enable lint checks for your release builds.

gradle=build -x lint

Option 2. When using Android Studio to run your project, pass -x lint in your command-line options under Preferences.

x-lint

Option 3. Pass -x lint via the command line when executing a Gradle task.

For example, ./gradlew assembleDebug -x lint.

Disable multi-APK generation

Google Play Store allows us to publish multiple APKs for specific device configurations. splits the block allows us to configure the Multiple APK support.

This is a sample configuration for multi-APK support.

android {
    splits {
        density {
            enable true
            exclude 'ldpi', 'xxxhdpi'
            compatibleScreens 'small', 'xlarge'
        }
    }
}

However, we don't need to generate multiple APKs during development.

if (project.hasProperty('devBuild')) {
    // Prevent multi apk generation on development
    splits.abi.enable = false
    splits.density.enable = false
}
Gradle executes the project's build file against the Project instance to configure the project.

You have to add -PdevBuild to your command to trigger the block with a property check of the project instance.

For example, ./gradlew assembleDebug -PdevBuild.

Or, when using Android Studio to run your project, pass it in your command-line options under Preferences.

pdevbuild

Estimates that this will reduce 5-10% of your build time.

More on multiple APKs.

Include minimal resources

Avoid compiling unnecessary resources that you aren't testing. Including, but not limited to, additional language localizations and screen density resources.

For development, you can optimize your project build time by specifying one language resource or screen density.

android {
    productFlavors {
        dev {
            ...
            resConfigs ("en", "xxhdpi")
        }
    }
}

Disable PNG crunching

Android performs automatic image compression every time you build your app, regardless of whether it is a release or debug build type. It helps reduce the size of your app by optimizing the images for release builds, but it will slow down project build times when you are in development.

Update: It's available since Android Studio 3.0 Canary 5 release.

PNG crunching is enabled for the release build and disabled by default for the debug build type.

android {
    buildTypes {
        release {
            // Disables PNG crunching on RELEASE build type
            crunchPngs false // Enabled by default for RELEASE build type
        }
    }
}

For older versions of the plugin

android {
    aaptOptions {
        cruncherEnabled false
    }
}

More on the crunchPngs feature.

Disable pre-dex Libraries

Gradle provides you with a DSL object for configuring dex options. One of the options that can be configured is preDexLibraries. You have the choice of whether you want your project to pre-dex libraries. Disabling pre-dex libraries can improve incremental builds but can slow down your project's clean builds.

If you wish to enable it, you can do this.

android {
    dexOptions {
        preDexLibraries true
    }
}

Configure wisely based on your development workflow preference. You should disable it when doing clean builds on your CI builds.

Disable Crashlytics

For every build, Crashlytics always generate a unique build ID. You can speed up your debug build by disabling the Crashlytics plugin.

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

Next, disable the kit at runtime for debug builds when initializing it.

val crashlytics = Crashlytics.Builder()
                .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
                .build()
Fabric.with(this, crashlytics)

But if you need to use Crashlytics on debug build, you can still improve your incremental builds by preventing it from updating app resources with its unique build ID every build.

android {
    buildTypes {
        debug {
            ext.alwaysUpdateBuildId = false
        }
    }
}

More on Crashlytics Build Tools.

Use static dependency versions

You must declare static or hard-coded version numbers for your dependencies in your build.gradle. It would be best to avoid dynamic dependencies represented by the plus sign (+). Otherwise, you might encounter unexpected version updates. The use of dynamic dependency declarations also slow your build as it checks for updates online.

android {
    dependencies {
        // What you should not do
        // implementation "androidx.paging:paging-runtime:2.+"

        // What you should do
        implementation "androidx.paging:paging-runtime:2.1.0"
    }
}

Enable build caching

By default, the build cache is not enabled. To use the build cache, you can invoke it by bypassing --build-cache on the command line when executing a Gradle task.

For example, ./gradlew assembleDebug --build-cache.

You can also configure build caching on your gradle.properties file.

org.gradle.caching=true // Add this line to enable build cache

Estimates that this will improve your clean build by up to 3 times faster and incremental builds by up to 10 times faster!

Configure the heap size for the JVM

The Gradle daemon now starts with 512MB of heap instead of 1GB. By default, this configuration may work best for a smaller project. But if you have a large project, you should update the heap size by setting org.gradle.jvmargsproperty in your gradle.properties file.

Here is the default configuration.

org.gradle.jvmargs=-Xmx512m "-XX:MaxMetaspaceSize=256m"

If you want to update the heap size to 2GB for larger projects.

org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

Bonus, you need to set the suitable file.encoding properties when the JVM executes the Gradle build (e.g. Gradle Daemon boot-up). In this example, we are putting it to UTF-8.

More on Gradle's default memory settings.

Modularize your project

Modularizing your project codebase allows the Gradle build system to compile only the modules you modify and cache those outputs for future builds.

Here's a quick overview of project modularization.

Modularization Sample 1

How the build system compiles your project based on module-specific changes.

Modularization Sample 2

I recommend modularization because of module re-use or optimizing build times and because it is ready to support the latest Android Dynamic Features and Instant App.

Here's an article by Joe Birch regarding the Modularization of Android Applications.

This is a detailed article on how modularization can improve your Android app's build time by Nikita Koslov.

Bonus

Use always-on Gradle daemon

By default, Gradle Daemon is enabled, but if the current project you manage disabled the daemon for every build, it annoys you -- you might want to update the configuration by allowing it.

org.gradle.daemon=true

Use parallel build execution

This configuration is effective only when your project is modularized. It will allow you to utilize the processing resources you have on your computer thoroughly.

org.gradle.parallel=true

More on parallel execution.

Conclusion

Keep your team updated with the latest Gradle releases, and look for possible API deprecation. Always profile your build, and adjust the configurations accordingly based on the results you get.