Sitemap
Dipien

Productivity tools & ideas for Android, Kotlin & Gradle developers.

30 ideas to reduce your Gradle Build times

Reduce your Gradle build times on Android & non-android projects with these great ideas

14 min readMay 15, 2023

--

Press enter or click to view image in full size

Is your Gradle build taking too long time? These 30 ideas can help you to optimize your slow Gradle build on Android and non-android projects.

1. Use the latest dependencies versions

If you stay up to date with your dependencies, you will get all the enhancements and optimizations they have to offer. Using the latest Gradle, Android Gradle plugin & Kotlin Gradle plugin versions can help a lot to reduce your build speed. But other dependencies upgrades, mainly Gradle plugins, are also important.

Keeping your Gradle project dependencies up to date can be a huge manual task if you have a big project. It’s a bit tedious for developers to manually check for dependencies upgrades, causing a lot of waste of time. The Releases Hub Gradle Plugin helps developers to keep their dependencies up to date.

You can read more about the plugin, in this post:

2. Enable Gradle Build scans

A build scan is a shareable record of a build that provides insights into what happened and why.

Enable Gradle Build scans for all your builds (local & ci builds), so you can analyze the reports and find potential issues on your build.

3. Avoid mixing Java & Kotlin code

Mixing Java & Kotlin code in the same module is bad for build times.

Here you can find a Uber post that talks about the topic.

You should pick any of both languages (I would vote for Kotlin) and migrate 100% of your code.

4. Stop generating the BuildConfig on your Android modules [Android only]

Generating the BuildConfig class for Android library modules is a bad idea. It causes build time penalties and also stimulates bad code patterns.

Since AGP 8.0, the generation is disabled by default !!!!

If you call the BuildConfig class from your module code, you need to enable buildConfig in the android {} block in your module’s build.gradle.kts file.

You can read this post for more details:

5. Deactivate Android Gradle Plugin unused features [Android only]

Android Gradle plugin 4.0.0 introduced a way to control which build features you want to enable and disable, such as View Binding and Data Binding. When new features are added, they will be disabled, by default.

You can enable only the features you want, and it helps you optimize the build performance for your project.

You can specify the default setting for these features across all modules in a project by including one or more of the following in your project’s gradle.properties file, as shown below.

# Flag to enable AIDL compilation. Determines whether to generate 
# binder classes for your AIDL files
android.defaults.buildfeatures.aidl=true
# Flag to enable/disable generation of the BuildConfig class
android.defaults.buildfeatures.buildconfig=true
# Flag to enable Compose feature
android.defaults.buildfeatures.compose=false
# Flag to enable Data Binding
# Note that the dataBinding.enabled property is now deprecated
android.defaults.buildfeatures.dataBinding=false
# Flag to enable/disable import of Prefab dependencies from AARs
android.defaults.buildfeatures.prefab=false
# Flag to enable RenderScript compilation
android.defaults.buildfeatures.renderscript=true
# Flag to enable Resource Values generation. Determines whether to
# support injecting custom variables into the module's R class.
android.defaults.buildfeatures.resvalues=true
# Flag to enable Shader AOT compilation
android.defaults.buildfeatures.shaders=true
# Flag to enable View Binding
# Note that the dataBinding.enabled property is now deprecated
android.defaults.buildfeatures.viewBinding=false

You can use the buildFeatures block in the module-level build.gradle file to override these project-wide default settings.

android {
// The default value for each feature is shown below.
buildFeatures {
aidl = true // false since AGP 8.0
buildConfig = true // false since AGP 8.0
compose = false
dataBinding = false
prefab = false
renderscript = true // false since AGP 8.0
resvalues = true
shaders = true
viewBinding = false
}
}

6. Remove unused code

If you have unused code in your project, each developer is wasting time understanding and compiling it.

Removing your unused code should be one of your main technical priorities.

7. Remove unused resources [Android only]

Similar to having unused code, if you have unused resources in your project, each developer computer is wasting time processing them.

This incredible plugin can help you automatically remove unused Android resources from your project.

You can configure your CI tool to regularly send a pull request with all the removed resources.

8. Apply Gradle Doctor plugin

The Gradle Doctor plugin analyzes your Gradle configuration and automatically detects issues on build time.

If you apply it, you can get some useful warnings related to build speed problems.

9. Disable Jetifier [Android only]

Having Jetifier enabled on your project (android.enableJetifier = true on your gradle.properties) impacts on the build speed, because the Android Gradle Plugin has to transform all the support-library-dependent artifacts to AndroidX. So, disabling jetifier is a good idea for your android project if you want to speed up your build speed. But doing that is not always a trivial task. In this article, you will find a list of 6 steps to follow, so you can fully migrate your app to AndroidX and then safely disable Jetifier.

10. Apply Android cache fix Gradle plugin [Android only]

Some Android plugin versions have issues with Gradle’s build cache feature. When applied to an Android project, the Android cache fix Gradle plugin applies workarounds for these issues based on the Android plugin and Gradle versions.

11. Disable Gradle plugins

Every time you apply a plugin to a project, you are increasing the Gradle configuration time.

Given that lots of plugins are only needed by certain environments, applying them wisely is a good idea to speed up your build times.

This 5 steps guide, shows you how to easily enable/disable plugins for each environment, so you can improve your Gradle configuration time.

12. Switch to Gradle Binary Distribution on your CI environment

The Gradle distribution zip file comes in two flavors:

  • Binary-only (-bin.zip)
  • Complete, with docs and sources (-all.zip)

In your development environment, is a good idea to download the complete distribution, so you can see the sources while developing and debugging your Gradle scripts.

So, your gradle/wrapper/gradle-wrapper.properties file should look like this:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

But you don’t need the complete distribution on your CI environment. It takes more time to download and use more disk storage. So, you can execute the following simple script before running any Gradle task on your CI environment, to switch to Gradle Binary Distribution:

sed -i -e 's/-all.zip/-bin.zip/' gradle/wrapper/gradle-wrapper.properties

13. Use the Gradle Daemon

A Gradle Daemon is a long-lived background process that executes your builds much more quickly than would otherwise be the case. Gradle accomplish this by avoiding the expensive bootstrapping process as well as leveraging caching, by keeping data about your project in memory.

The Gradle Daemon is enabled by default starting with Gradle 3.0, so you don’t have to do anything to benefit from it. So, try to avoid:

  • using the --no-daemon flag on command-line
  • defining org.gradle.daemon=false on your gradle.properties file

14. JDK consistency

Using a different JDK for command-line builds vs Android Studio / IntelliJ builds will cause a new Gradle daemon to spawn. This will instantly double the memory being used by Gradle.

Also, sometimes the Gradle build cache doesn’t work properly across different major JDK versions. For example, two builds targeting JDK 8 and 11 respectively will produce incompatible cache entries.

Having JDK consistency on all the places you run a Gradle task is recommended. Some common places:

  • Command-line (local & CI environment)
  • Android Studio / IntelliJ IDEA
  • GIT clients like Sourcetree (if you have git hooks that run Gradle tasks)

You should set your JAVA_HOME environment variable and then use this JAVA_HOME as the JDK used by Android Studio or IntelliJ.

Configure JDK on Android Studio

  1. Go to Android Studio -> Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle
  2. Click on Gradle JDK
  3. Pick the same JDK referenced by your JAVA_HOME environment variable
Press enter or click to view image in full size

15. Filters on Gradle Repositories declarations

If you declared multiple repositories on your Gradle configuration, by default each dependency will be searched on all of them. This causes some delays when resolving the dependencies because Gradle will search for some of them on repositories that don’t have them.

Gradle provides an API that lets you declare that a repository exclusively includes an artifact. If you do so:

  • an artifact declared in a repository can’t be found in any other
  • exclusive repository content must be declared in extension (just like for repository-level content)

For example:

repositories {
// This repository will not be searched for artifacts with group "my.company"
// despite being declared first
mavenCentral()
exclusiveContent {
forRepository {
maven {
url = uri("https://repo.mycompany.com/maven2")
}
}
filter {
// this repository only contains artifacts with group "my.company"
includeGroup("my.company")
}
}
}

16. Modularize your app

Modularization is a big topic and gives us multiple benefits. One of them is the reduction of your build times. If you have multiple decoupled modules, Gradle can run their tasks in parallel, saving a lot of time.

When possible, having java or kotlin only modules (I mean, without Android dependencies) is a good idea because you can save some time not applying the Android Gradle plugin to that module.

17. Avoid enabling R8 on debug [Android only]

The Android Gradle Plugin works with the R8 compiler (instead of Proguard since v3.4.0) to handle the following compile-time tasks:

  • Code shrinking (or tree-shaking): detects and safely removes unused classes, fields, methods, and attributes
  • Resource shrinking: removes unused resources
  • Obfuscation: shortens the name of classes and members
  • Optimization: inspects and rewrites your code to further reduce the size of your app’s DEX files

R8 consumes a lot of time optimizing your code. So, verify that you are not enabling it on your debug build type. TheminifyEnabled & shrinkResources properties shouldn’t appear on your debug build type or they could appear but with false value.

18. Slower From Cache (Negative Savings)

If you have a remote Gradle Build Cache server running, you will find some cases where downloading the cache takes more time than executing the task locally. Sometimes this is caused by your internet connection speed but in other cases, the problem is the task itself. Here there are two excellent articles that talk about this topic more deeply:

The Gradle Doctor plugin can help you to detect those tasks:

Once you find those tasks, you can add something like this to your root build.gradle to disable the caching for them:

def tasksToNotCache = [
"compile.*JavaWithJavac",
"compile.*LibraryResources",
"package.*Resources",
"strip.*DebugSymbols",
"mergeDebugAndroidTestNativeLibs",
"generateDebugRFile",
"dataBindingMergeDependencyArtifacts.*",
"dataBindingMergeGenClasses.*",
"bundleLibRuntimeToJarDebug",
"bundleLibCompileToJarDebug",
"parse.*LocalResources"
]
allprojects {
afterEvaluate {
tasks.matching {
task -> tasksToNotCache.any { task.name ==~ it }
}.configureEach {
outputs.cacheIf { false }
}
}
}

19. Migrate your images resources to WEBP [Android only]

WebP is an image file format from Google that provides lossy compression (like JPEG) as well as transparency (like PNG) but can provide better compression than either JPEG or PNG. Lossy WebP images are supported in Android 4.0 (API level 14) and higher, and lossless and transparent WebP images are supported in Android 4.3 (API level 18) and higher.

Migrating your images on your Android project to WEBP is a good idea for reducing your APK size. And as a side effect, you can also save a bit of time when installing your APK on your device during development.

20. Use specific dependencies versions

A deterministic build is one that can be run “live” at commit time, tomorrow, and even next month and end with the exact same results. You can imagine taking a “fingerprint” of a build from when it first ran, and doing so again on a re-run. They should match exactly.

You can read more about deterministic builds here:

Using dynamic versions (such as 1.0.+), or snapshot versions (such as 1.0.0-SNAPSHOT) is a bad practice because you can’t guarantee deterministic builds. They also force Gradle to contact the remote repository to find out whether there’s a new version or snapshot available. Instead, use fixed version, so your builds are faster and more deterministic.

You can find all dependencies with dynamic versions via build scans:

Press enter or click to view image in full size
Find dependencies with dynamic versions

21. Upgrade your development tools

If you stay always up to date with your development tools, you will get all the latest performance improvements. Just be sure you are using the latest version of the different development tools you use, like:

  • Android Studio
  • Kotlin plugin on Android Studio
Press enter or click to view image in full size
  • Android SDK Tools
Press enter or click to view image in full size
  • Gradle
  • JVM

22. API vs implementation

A lot of the time, you are only changing internal implementation details of your code, e.g. editing a method body. These so-called ABI-compatible changes no longer trigger the recompilation of downstream projects. This especially improves build times in large multi-project builds with deep dependency chains.

You can clearly separate which dependencies are part of your api and which are only implementation details. Implementation dependencies are not leaked into the compile classpath of downstream projects, which means that they will no longer be recompiled when such an implementation detail changes.

dependencies {
api(project("my-utils"))
implementation("com.google.guava:guava:21.0")
}

This is not only a performance tip, you also improve your code quality by doing this.

You can read more about this in the official documentation of compile avoidance.

23. Avoid compiling unnecessary resources [Android only]

Avoid compiling and packaging resources that you aren’t testing (such as additional language localizations). You can do that by only specifying one language resource and screen density for your development environment but keep all of them on your CI environment, as shown in the following sample:

android {
defaultConfig {
if (System.getenv("CI") != "true") {
resConfigs = project.property("resConfigsOnLocalEnv")
}
}
}

Then you can define the following on your gradle.properties file

resConfigsOnLocalEnv = "en"

24. Non-transitive R classes [Android only — AGP < 8.0]

If you are using an Android Gradle Plugin version older than 8.0, then set android.nonTransitiveRClass=true on your gradle.properties file. From AGP 8.0, it’s true when not specified, and thus becomes the default.

This flag enables namespacing of each library’s R class so that its R class includes only the resources declared in the library itself and none from the library’s dependencies, thereby reducing the size of the R class for that library.

Press enter or click to view image in full size

You can automatically refactor your project on Android Studio, running Refactor -> Migrate to Non-transitive R Classes

Press enter or click to view image in full size

25. Remove unused dependencies

Having unused dependencies declared on your grade configuration files increases your build times. You can use the gradle-lint-plugin to detect those unused dependencies. Just add the following configuration to your root build.gradle file:

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.netflix.nebula:gradle-lint-plugin:{VERSION}'
}
}
allprojects {
apply plugin: 'nebula.lint'
gradleLint.rules = ['all-dependency']
}

And then run the lintGradle task. You will see a report with all the unused dependencies, as well as other useful Gradle warnings.

26. Parallel execution

By adding the following line to your gradle.properties you can force Gradle to execute tasks in parallel as long as those tasks are in different projects.

org.gradle.parallel=true

27. Use static build config values with your debug build [Android only]

Always use static/hard-coded values for properties that go in the manifest file or resource files for your debug build type.

For example, using dynamic version codes, version names, resources, or any other build logic that changes the manifest file requires a full app build every time you want to run a change — even though the actual change might otherwise require only a hot swap. If your build configuration requires such dynamic properties, then isolate them to your release build variants and keep the values static for your debug builds.

28. Enable Gradle Configuration Cache

The configuration cache is a feature that significantly improves build performance by caching the result of the configuration phase and reusing this for subsequent builds. Using the configuration cache, Gradle can skip the configuration phase entirely when nothing that affects the build configuration, such as build scripts, has changed.

You can read more about this in the official documentation.

29. Disable unused variants [Android only]

Every time you create a new variant on an Android project, Gradle creates lots of new tasks related to that variant, increasing the configuration time. Some variants are not needed in development environments, so you can disable them and only enable them when needed.

For example:

buildTypes {
if (project.property("instrumentationBuildTypeEnabled") == true) {
instrumentation {
...
}
}
}

30. Disable PNG crunching [Android only]

If you can’t (or don’t want to) convert your PNG images to WebP, you can still speed up your build by disabling automatic image compression every time you build your app.

PNG crunching is disabled by default for only the debug build type. To disable this optimization for other build types, add the following to your build.gradle file:

android {
buildTypes {
release {
// Disables PNG crunching for the release build type.
crunchPngs false
}
}
}

Support Us

There are different ways to support our work:

  • With Bitcoin Lightning using Alby:
  • With PayPal or a credit card using Ko-fi.

--

--

Dipien
Dipien

Published in Dipien

Productivity tools & ideas for Android, Kotlin & Gradle developers.

Maxi Rosson
Maxi Rosson

Written by Maxi Rosson

I write about Productivity tools & ideas for Android, Kotlin & Gradle developers. Founder of smarthomecompared.com where I compare the best Smart Home device.