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 builds.
DISCLAIMER: Some of the tips will include estimations, these estimations may not exactly match your project's build time improvement. Several factors affect the estimations -- including, but not limited to, your project's architecture, number of tests written, lint checks, current gradle configuration, your computer's hardware specifications, and the list goes on.
With that said, I hope this list will improve your overall team's productivity when building awesome apps!
Improving Your Project Build Time
1. Profile, profile, profile!
Profile your build, numbers don't lie. As we may have different project architecture, tests, and way of writing code, it's best that you always profile your own build -- preferrably on each optimization configuration change you make. You can pass on --profile
directly on the command-line when invoking Gradle task.
For example, ./gradlew assembleDebug --profile
.
Or, you can add it on the command-line options under Preferences when running the project on Android Studio.
This is a sample report after running the task with --profile
.
2. Always use the latest Gradle Plugin for Android
Gradle team release newer versions of Gradle with faster compilation and build times. At the time of writing, the latest release currently available is Gradle 5.4.1.
Note, newer version changes may introduce unwanted compilation errors or compatibility issues, make sure to check out and follow the migration guide when upgrading your Gradle.
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.
3. Avoid Legacy Multidex
A brief introduction on 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 reached the limit, you will encounter a build error. Multidex was introduced to help you avoid the 64K limit.
To support multidex, just add and set multiDexEnabled
to true
.
android {
defaultConfig {
...
minSdkVersion 21
targetSdkVersion 28
multiDexEnabled true
}
}
If your minSdkVersion
is set to 20 or lower, then 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 now 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.
More on multidex.
4. Disable lint checks on development
NOTE: IMHO, I only recommend this step for a team that have computers with lower specs and does not have a strict requirement on lint checks during development.
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 you are 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 you are 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.
Option 3. Pass -x lint
via command-line when executing a Gradle task.
For example, ./gradlew assembleDebug -x lint
.
5. Disable multi-APK generation
Google Play Store allows us to publish multiple APKs for specific device configuration. splits
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.
Note, you have to add -PdevBuild
to your command to trigger the block with property check of the project instance.
For example, ./gradlew assembleDebug -PdevBuild
.
Or, when you are using Android Studio to run your project, pass it in your command-line options under Preferences.
Estimates that this will reduce 5-10% of your build time.
More on multiple APKs.
6. 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")
}
}
}
7. Disable PNG crunching
Android performs automatic image compression every time you build your app regardless 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 on development.
Update: It's available since Android Studio 3.0 Canary 5 release.
PNG crunching is enabled by default 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 crunchPngs.
8. Configure DexOptions wisely
Gradle provides you a DSL object for configuring dex options. One of the options that can be configured is preDexLibraries
. You have the choice on whether you want your project to pre-dex libraries. Take note that this can improve incremental builds, but can slow down your 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 when doing clean builds on your CI builds.
9. Use Crashlytics only when needed
Every build, Crashlytics always generate 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 own unique build ID every build.
android {
buildTypes {
debug {
ext.alwaysUpdateBuildId = false
}
}
}
More on Crashlytics Build Tools.
10. Use static dependency versions
You must declare static or hard-coded version numbers for your dependencies in your build.gradle
. You should avoid dynamic dependencies, represented by using plus sign (+), otherwise you might encounter unexpected version updates. Dynamic dependency declarations also slower your builds as it continue checking 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"
}
}
11. Enable build caching
By default, build cache is not enabled. To use build cache, you can invoke it by passing --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 full clean build by up to 3 times faster, and incremental builds by up to 10 times faster!
12. Configure the heap size for the JVM
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.jvmargs
property 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 right file.encoding
properties when the JVM executes the Gradle build (eg. Gradle Daemon boot up). In this example, we are setting it to UTF-8.
More on Gradle's default memory settings.
13. 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.
How the build system compiles your project based on module specific changes.
I recommend modularization not only because of module re-use, or for optimizing build times, but also because it is ready to support the latest Android's Dynamic Features and Instant App.
Here's an article by Joe Birch regarding Modularization of Android Applications.
This is a detailed article how modularization can improve your Android app's build time by Nikita Koslov.
14. Runners-up: Always-on daemon, parallel build execution, and configure on demand.
Gradle Daemon
By default Gradle Daemon is enabled, but if the current project you are managing disabled the daemon for the every build and it annoys you -- you might want to update the configuration by enabling it.
org.gradle.daemon=true
Parallel Build Execution
This configuration is effective only when your project is modularized. It will allow you to fully utilize the processing resources you have in your computer.
org.gradle.parallel=true
More on parallel execution.
Configure On Demand
You will only benefit from this configuration if you have a multi-project builds that have decoupled projects -- take note, it's multi-project, not just a modularized project.
Let's make an example out of the Google I/O 2018 Android App.
Notice the project structure, it includes various projects such as mobile
, and tv
. Configure on demand will attempt to configure only the projects that are relevant for the requested tasks (eg. configure only mobile
for the requested task).
NOTE: The configuration on demand feature is incubating so not every build is guaranteed to work correctly.
More on configure on demand.
Conclusion
Keep your team updated with the latest Gradle releases, and lookout for the possible API deprecation. Always profile your build, and adjust the configurations accordingly based on the results you get.
With these tips, I hope you will significantly improve your team's productivity and help each other make great apps again!
If you have questions or suggestions, feel free to comment below or ask me on my twitter @devjdg.
Happy coding!
Top comments (6)
Thanks for the post! So many great tips.
I'd like to add that you can get even more profiling with free build scans by adding --scan on the command line or following the instructions at scans.gradle.com.
And for large enterprise organizations, for example, teams with 100+ devs, the shared remote cache in Gradle Enterprise can go even further to reduce Maven and Gradle build speeds by up to 90%.
Thanks for the additional input! Will check this out too! Cheers.
Very good tips. Got my build to go from 3m to 1m! Another tip I can give you is to disable Firebase Performance plugin for develop, like:
That would take at least 1m from your build time ;)
Excelent tips! In our case, disabling Firebase Performance plugin for debug builds has decreased our build times another ~20-40%. Any applied plugin in the Gradle script can change your task resolution.
Adjusting the heap size of my JVM had a huge performance hike in my Android studio. I have 16gb of ram in my PC, but with the default settings Android studio was only using 3gb.
Glad it helped!