Improve the Performance of Gradle Builds
- Inspect your build
- Update versions
- Enable parallel execution
- Re-enable the Gradle Daemon
- Enable the configuration cache
- Enable incremental build for custom tasks
- Enable the build cache
- Create builds for specific developer workflows
- Increase the heap size
- Optimize Configuration
- Optimize Dependency resolution
- Optimize Java projects
- Improve the performance of older Gradle releases
- Optimize Android projects
Build performance is critical to productivity. The longer builds take to complete, the more likely they’ll disrupt your development flow. Builds run many times a day, so even small waiting periods add up. The same is true for Continuous Integration (CI) builds: the less time they take, the faster you can react to new issues and the more often you can experiment.
All this means that it’s worth investing some time and effort into making your build as fast as possible. This section offers several ways to make a build faster. Additionally, you’ll find details about what leads to build performance degradation, and how you can avoid it.
Want faster Gradle Builds? Register here for our Build Cache training session to learn how Develocity can speed up builds by up to 90%. |
Inspect your build
Before you make any changes, inspect your build with a build scan or profile report. A proper build inspection helps you understand:
-
how long it takes to build your project
-
which parts of your build are slow
Inspecting provides a comparison point to better understand the impact of the changes recommended on this page.
To best make use of this page:
-
Inspect your build.
-
Make a change.
-
Inspect your build again.
If the change improved build times, make it permanent. If you don’t see an improvement, remove the change and try another.
Update versions
Gradle
The Gradle team continuously improves the performance of Gradle builds. If you’re using an old version of Gradle, you’re missing out on the benefits of that work. Keeping up with Gradle version upgrades is low risk because the Gradle team ensures backwards compatibility between minor versions of Gradle. Staying up-to-date also makes transitioning to the next major version easier, since you’ll get early deprecation warnings.
Java
Gradle runs on the Java Virtual Machine (JVM). Java performance improvements often benefit Gradle. For the best Gradle performance, use the latest version of Java.
Plugins
Plugin writers continuously improve the performance of their plugins. If you’re using an old version of a plugin, you’re missing out on the benefits of that work. The Android, Java, and Kotlin plugins in particular can significantly impact build performance. Update to the latest version of these plugins for performance improvements.
Enable parallel execution
Most projects consist of more than one subproject. Usually, some of those subprojects are independent of one another;
that is, they do not share state. Yet by default, Gradle only runs one task at a time.
To execute tasks belonging to different subprojects in parallel, use the parallel
flag:
$ gradle <task> --parallel
To execute project tasks in parallel by default, add the following setting to the gradle.properties
file in the project root or your Gradle home:
org.gradle.parallel=true
Parallel builds can significantly improve build times; how much depends on your project structure and how many dependencies you have between subprojects. A build whose execution time is dominated by a single subproject won’t benefit much at all. Neither will a project with lots of inter-subproject dependencies. But most multi-subproject builds see a reduction in build times.
Visualize parallelism with build scans
Build scans give you a visual timeline of task execution. In the following example build, you can see long-running tasks at the beginning and end of the build:
Tweaking the build configuration to run the two slow tasks early on and in parallel reduces the overall build time from 8 seconds to 5 seconds:
Re-enable the Gradle Daemon
The Gradle Daemon reduces build times by:
-
caching project information across builds
-
running in the background so every Gradle build doesn’t have to wait for JVM startup
-
benefiting from continuous runtime optimization in the JVM
-
watching the file system to calculate exactly what needs to be rebuilt before you run a build
Gradle enables the Daemon by default, but some builds override this preference. If your build disables the Daemon, you could see a significant performance improvement from enabling the daemon.
You can enable the Daemon at build time with the daemon
flag:
$ gradle <task> --daemon
To enable the Daemon by default in older Gradle versions, add the following setting to the
gradle.properties
file in the project root or your Gradle home:
org.gradle.daemon=true
On developer machines, you should see a significant performance improvement. On CI machines, long-lived agents benefit from the Daemon. But short-lived machines don’t benefit much. Daemons automatically shut down on memory pressure in Gradle 3.0 and above, so it’s always safe to leave the Daemon enabled.
Enable the configuration cache
This feature has the following limitations:
|
You can cache the result of the configuration phase by enabling the configuration cache. When build configuration inputs remain the same across builds, the configuration cache allows Gradle to skip the configuration phase entirely.
Build configuration inputs include:
-
Init scripts
-
Settings scripts
-
Build scripts
-
System properties used during the configuration phase
-
Gradle properties used during the configuration phase
-
Environment variables used during the configuration phase
-
Configuration files accessed using value suppliers such as providers
-
buildSrc
inputs, including build configuration inputs and source files
By default, Gradle does not use the configuration cache.
To enable the configuration cache at build time, use the configuration-cache
flag:
$ gradle <task> --configuration-cache
To enable the configuration cache by default, add the following setting to the gradle.properties
file in the project root or your Gradle home:
org.gradle.configuration-cache=true
For more information about the configuration cache, check out the configuration cache documentation.
Additional configuration cache benefits
The configuration cache enables additional benefits as well. When enabled, Gradle:
-
Executes all tasks in parallel, even those in the same subproject.
-
Caches dependency resolution results.
Enable incremental build for custom tasks
Incremental build is a Gradle optimization that skips running tasks that have previously executed with the same inputs. If a task’s inputs and its outputs have not changed since the last execution, Gradle skips that task.
Most built-in tasks provided by Gradle work with incremental build. To make a custom task compatible with incremental build, specify the inputs and outputs:
tasks.register("processTemplatesAdHoc") {
inputs.property("engine", TemplateEngineType.FREEMARKER)
inputs.files(fileTree("src/templates"))
.withPropertyName("sourceFiles")
.withPathSensitivity(PathSensitivity.RELATIVE)
inputs.property("templateData.name", "docs")
inputs.property("templateData.variables", mapOf("year" to "2013"))
outputs.dir(layout.buildDirectory.dir("genOutput2"))
.withPropertyName("outputDir")
doLast {
// Process the templates here
}
}
tasks.register('processTemplatesAdHoc') {
inputs.property('engine', TemplateEngineType.FREEMARKER)
inputs.files(fileTree('src/templates'))
.withPropertyName('sourceFiles')
.withPathSensitivity(PathSensitivity.RELATIVE)
inputs.property('templateData.name', 'docs')
inputs.property('templateData.variables', [year: '2013'])
outputs.dir(layout.buildDirectory.dir('genOutput2'))
.withPropertyName('outputDir')
doLast {
// Process the templates here
}
}
For more information about incremental builds, check out the incremental build documentation.
Visualize incremental builds with build scan timelines
Look at the build scan timeline view to identify tasks that could benefit from incremental builds. This can also help you understand why tasks execute when you expect Gradle to skip them.
As you can see in the build scan above, the task was not up-to-date because one of its inputs ("timestamp") changed, forcing the task to re-run.
Sort tasks by duration to find the slowest tasks in your project.
Enable the build cache
The build cache is a Gradle optimization that stores task outputs for specific input.
When you later run that same task with the same input, Gradle retrieves the output from the build cache instead of running the task again.
By default, Gradle does not use the build cache.
To enable the build cache at build time, use the build-cache
flag:
$ gradle <task> --build-cache
To enable the build cache by default, add the following setting to the gradle.properties
file in the project root or your Gradle home:
org.gradle.caching=true
You can use a local build cache to speed up repeated builds on a single machine. You can also use a shared build cache to speed up repeated builds across multiple machines. Develocity provides one. Shared build caches can decrease build times for both CI and developer builds.
For more information about the build cache, check out the build cache documentation.
Visualize the build cache with build scans
Build scans can help you investigate build cache effectiveness. In the performance screen, the "Build cache" tab shows you statistics about:
-
how many tasks interacted with a cache
-
which cache was used
-
transfer and pack/unpack rates for these cache entries
The "Task execution" tab shows details about task cacheability. Click on a category to see a timeline screen that highlights tasks of that category.
Sort by task duration on the timeline screen to highlight tasks with great time saving potential.
The build scan above shows that :task1
and :task3
could be improved and made cacheable
and shows why Gradle didn’t cache them.
Create builds for specific developer workflows
The fastest task is one that doesn’t execute. If you can find ways to skip tasks you don’t need to run, you’ll end up with a faster build overall.
If your build includes multiple subprojects, create tasks to build those subprojects independently. This helps you get the most out of caching, since a change to one subproject won’t force a rebuild for unrelated subprojects. And this helps reduce build times for teams that work on unrelated subprojects: there’s no need for front-end developers to build the back-end subprojects every time they change the front-end. Documentation writers don’t need to build front-end or back-end code even if the documentation lives in the same project as that code.
Instead, create tasks that match the needs of developers. You’ll still have a single task graph for the whole project. Each group of users suggests a restricted view of the task graph: turn that view into a Gradle workflow that excludes unnecessary tasks.
Gradle provides several features to create these workflows:
-
Assign tasks to appropriate groups
-
Create aggregate tasks: tasks with no action that only depend on other tasks, such as
assemble
-
Defer configuration via
gradle.taskGraph.whenReady()
and others, so you can perform verification only when it’s necessary
Increase the heap size
By default, Gradle reserves 512MB of heap space for your build. This is plenty for most projects.
However, some very large builds might need more memory to hold Gradle’s model and caches.
If this is the case for you, you can specify a larger memory requirement.
Specify the following property in the gradle.properties
file in your project root or your Gradle home:
org.gradle.jvmargs=-Xmx2048M
To learn more, check out the JVM memory configuration documentation.
Optimize Configuration
As described in the build lifecycle chapter, a
Gradle build goes through 3 phases: initialization, configuration, and execution.
Configuration code always executes regardless of the tasks that run.
As a result, any expensive work performed during configuration slows down every invocation.
Even simple commands like gradle help
and gradle tasks
.
The next few subsections introduce techniques that can reduce time spent in the configuration phase.
You can also enable the configuration cache to reduce the impact of a slow configuration phase. But even machines that use the cache still occasionally execute your configuration phase. As a result, you should make the configuration phase as fast as possible with these techniques. |
Avoid expensive or blocking work
You should avoid time-intensive work in the configuration phase.
But sometimes it can sneak into your build in non-obvious places.
It’s usually clear when you’re encrypting data or calling remote services during configuration if that code is in a build file.
But logic like this is more often found in plugins and occasionally custom task classes.
Any expensive work in a plugin’s apply()
method or a tasks’s constructor is a red flag.
Only apply plugins where they’re needed
Every plugin and script that you apply to a project adds to the overall configuration time.
Some plugins have a greater impact than others.
That doesn’t mean you should avoid using plugins, but you should take care to only apply them where they’re needed.
For example, it’s easy to apply plugins to all subprojects via allprojects {}
or subprojects {}
even if not every project needs them.
In the above build scan example, you can see that the root build script applies the script-a.gradle
script to 3 subprojects inside the build:
This script takes 1 second to run. Since it applies to 3 subprojects, this script cumulatively delays the configuration phase by 3 seconds. In this situation, there are several ways to reduce the delay:
-
If only one subproject uses the script, you could remove the script application from the other subprojects. This reduces the configuration delay by two seconds in each Gradle invocation.
-
If multiple subprojects, but not all, use the script, you could refactor the script and all surrounding logic into a custom plugin located in
buildSrc
. Apply the custom plugin to only the relevant subprojects, reducing configuration delay and avoiding code duplication.
Statically compile tasks and plugins
Plugin and task authors often write Groovy for its concise syntax, API extensions to the JDK, and functional methods using closures. But Groovy syntax comes with the cost of dynamic interpretation. As a result, method calls in Groovy take more time and use more CPU than method calls in Java or Kotlin.
You can reduce this cost with static Groovy compilation: add the @CompileStatic
annotation to your Groovy classes when you don’t
explicitly require dynamic features. If you need dynamic Groovy in a method, add the @CompileDynamic
annotation to that method.
Alternatively, you can write plugins and tasks in a statically compiled language such as Java or Kotlin.
Warning: Gradle’s Groovy DSL relies heavily on Groovy’s dynamic features. To use static compilation in your plugins, switch to Java-like syntax.
The following example defines a task that copies files without dynamic features:
project.tasks.register('copyFiles', Copy) { Task t ->
t.into(project.layout.buildDirectory.dir('output'))
t.from(project.configurations.getByName('compile'))
}
This example uses the register()
and getByName()
methods available on all Gradle “domain object containers”.
Domain object containers include tasks, configurations, dependencies, extensions, and more.
Some collections, such as TaskContainer
, have dedicated types with extra methods like create,
which accepts a task type.
When you use static compilation, an IDE can:
-
quickly show errors related to unrecognised types, properties, and methods
-
auto-complete method names
Optimize Dependency resolution
Dependency resolution simplifies integrating third-party libraries and other dependencies into your projects. Gradle contacts remote servers to discover and download dependencies. You can optimize the way you reference dependencies to cut down on these remote server calls.
Avoid unnecessary and unused dependencies
Managing third-party libraries and their transitive dependencies adds a significant cost to project maintenance and build times.
Watch out for unused dependencies: when a third-party library stops being used by isn’t removed from the dependency list. This happens frequently during refactors. You can use the Gradle Lint plugin to identify unused dependencies.
If you only use a small number of methods or classes in a third-party library, consider:
-
implementing the required code yourself in your project
-
copying the required code from the library (with attribution!) if it is open source
Optimize repository order
When Gradle resolves dependencies, it searches through each repository in the declared order. To reduce the time spent searching for dependencies, declare the repository hosting the largest number of your dependencies first. This minimizes the number of network requests required to resolve all dependencies.
Minimize repository count
Limit the number of declared repositories to the minimum possible for your build to work.
If you’re using a custom repository server, create a virtual repository that aggregates several repositories together. Then, add only that repository to your build file.
Minimize dynamic and snapshot versions
Dynamic versions (e.g. “2.+”), and changing versions (snapshots) force Gradle to contact remote repositories to find new releases. By default, Gradle only checks once every 24 hours. But you can change this programmatically with the following settings:
-
cacheDynamicVersionsFor
-
cacheChangingModulesFor
If a build file or initialization script lowers these values, Gradle queries repositories more often. When you don’t need the absolute latest release of a dependency every time you build, consider removing the custom values for these settings.
Find dynamic and changing versions with build scans
You can find all dependencies with dynamic versions via build scans:
You may be able to use fixed versions like "1.2" and "3.0.3.GA" that allow Gradle to cache versions. If you must use dynamic and changing versions, tune the cache settings to best meet your needs.
Avoid dependency resolution during configuration
Dependency resolution is an expensive process, both in terms of I/O and computation. Gradle reduces the required network traffic through caching. But there is still a cost. Gradle runs the configuration phase on every build. If you trigger dependency resolution during the configuration phase, every build pays that cost.
Switch to declarative syntax
If you evaluate a configuration file, your project pays the cost of dependency resolution during configuration. Normally tasks evaluate these files, since you don’t need the files until you’re ready to do something with them in a task action. Imagine you’re doing some debugging and want to display the files that make up a configuration. To implement this, you might inject a print statement:
tasks.register<Copy>("copyFiles") {
println(">> Compilation deps: ${configurations.compileClasspath.get().files.map { it.name }}")
into(layout.buildDirectory.dir("output"))
from(configurations.compileClasspath)
}
tasks.register('copyFiles', Copy) {
println ">> Compilation deps: ${configurations.compileClasspath.files.name}"
into(layout.buildDirectory.dir('output'))
from(configurations.compileClasspath)
}
The files
property forces Gradle to resolve the dependencies. In this example, that happens during the configuration phase.
Because the configuration phase runs on every build, all builds now pay the performance cost of dependency resolution.
You can avoid this cost with a doFirst()
action:
tasks.register<Copy>("copyFiles") {
into(layout.buildDirectory.dir("output"))
// Store the configuration into a variable because referencing the project from the task action
// is not compatible with the configuration cache.
val compileClasspath: FileCollection = configurations.compileClasspath.get()
from(compileClasspath)
doFirst {
println(">> Compilation deps: ${compileClasspath.files.map { it.name }}")
}
}
tasks.register('copyFiles', Copy) {
into(layout.buildDirectory.dir('output'))
// Store the configuration into a variable because referencing the project from the task action
// is not compatible with the configuration cache.
FileCollection compileClasspath = configurations.compileClasspath
from(compileClasspath)
doFirst {
println ">> Compilation deps: ${compileClasspath.files.name}"
}
}
Note that the from()
declaration doesn’t resolve the dependencies because you’re using the dependency configuration itself as an argument, not the files.
The Copy
task resolves the configuration itself during task execution.
Visualize dependency resolution with build scans
The "Dependency resolution" tab on the performance page of a build scan shows dependency resolution time during the configuration and execution phases:
Build scans provide another means of identifying this issue. Your build should spend 0 seconds resolving dependencies during "project configuration". This example shows the build resolves dependencies too early in the lifecycle. You can also find a "Settings and suggestions" tab on the "Performance" page. This shows dependencies resolved during the configuration phase.
Remove or improve custom dependency resolution logic
Gradle allows users to model dependency resolution in the way that best suits them. Simple customizations, such as forcing specific versions of a dependency or substituting one dependency for another, don’t have a big impact on dependency resolution times. More complex customizations, such as custom logic that downloads and parses POMs, can slow down dependency resolution signficantly.
Use build scans or profile reports to check that custom dependency resolution logic doesn’t adversely affect dependency resolution times. This could be custom logic you have written yourself, or it could be part of a plugin.
Remove slow or unexpected dependency downloads
Slow dependency downloads can impact your overall build performance. Several things could cause this, including a slow internet connection or an overloaded repository server. On the "Performance" page of a build scan, you’ll find a "Network Activity" tab. This tab lists information including:
-
the time spent downloading dependencies
-
the transfer rate of dependency downloads
-
a list of downloads sorted by download time
In the following example, two slow dependency downloads took 20 and 40 seconds and slowed down the overall performance of a build:
Check the download list for unexpected dependency downloads. For example, you might see a download caused by a dependency using a dynamic version.
Eliminate these slow or unexpected downloads by switching to a different repository or dependency.
Optimize Java projects
The following sections apply only to projects that use the java
plugin or another JVM language.
Optimize tests
Projects often spend much of their build time testing. These could be a mixture of unit and integration tests. Integration tests usually take longer. Build scans can help you identify the slowest tests. You can then focus on speeding up those tests.
The above build scan shows an interactive test report for all projects in which tests ran.
Gradle has several ways to speed up tests:
-
Execute tests in parallel
-
Fork tests into multiple processes
-
Disable reports
Let’s look at each of these in turn.
Execute tests in parallel
Gradle can run multiple test cases in parallel.
To enable this feature, override the value of maxParallelForks
on the relevant Test
task.
For the best performance, use some number less than or equal to the number of available CPU cores:
tasks.withType<Test>().configureEach {
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
}
tasks.withType(Test).configureEach {
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
}
Tests in parallel must be independent. They should not share resources such as files or databases. If your tests do share resources, they could interfere with each other in random and unpredictable ways.
Fork tests into multiple processes
By default, Gradle runs all tests in a single forked VM. If there are a lot of tests, or some tests that consume lots of memory, your tests may take longer than you expect to run. You can increase the heap size, but garbage collection may slow down your tests.
Alternatively, you can fork a new test VM after a certain number of tests have run with the forkEvery
setting:
tasks.withType<Test>().configureEach {
forkEvery = 100
}
tasks.withType(Test).configureEach {
forkEvery = 100
}
Forking a VM is an expensive operation. Setting too small a value here slows down testing. |
Disable reports
Gradle automatically creates test reports regardless of whether you want to look at them. That report generation slows down the overall build. You may not need reports if:
-
you only care if the tests succeeded (rather than why)
-
you use build scans, which provide more information than a local report
To disable test reports, set reports.html.required
and reports.junitXml.required
to false
in the Test
task:
tasks.withType<Test>().configureEach {
reports.html.required = false
reports.junitXml.required = false
}
tasks.withType(Test).configureEach {
reports.html.required = false
reports.junitXml.required = false
}
Conditionally enable reports
You might want to conditionally enable reports so you don’t have to edit the build file to see them. To enable the reports based on a project property, check for the presence of a property before disabling reports:
tasks.withType<Test>().configureEach {
if (!project.hasProperty("createReports")) {
reports.html.required = false
reports.junitXml.required = false
}
}
tasks.withType(Test).configureEach {
if (!project.hasProperty("createReports")) {
reports.html.required = false
reports.junitXml.required = false
}
}
Then, pass the property with -PcreateReports
on the command line to generate the reports.
$ gradle <task> -PcreateReports
Or configure the property in the gradle.properties
file in the project root or your Gradle home:
createReports=true
Optimize the compiler
The Java compiler is fast. But if you’re compiling hundreds of Java classes, even a short compilation time adds up. Gradle offers a several optimizations for Java compilation:
-
Run the compiler as a separate process
-
Switch internal-only dependencies to implementation visibility
Run the compiler as a separate process
You can run the compiler as a separate process with the following configuration for any JavaCompile
task:
<task>.options.isFork = true
<task>.options.fork = true
To apply the configuration to all Java compilation tasks, you can configureEach
java compilation task:
tasks.withType<JavaCompile>().configureEach {
options.isFork = true
}
tasks.withType(JavaCompile).configureEach {
options.fork = true
}
Gradle reuses this process within the duration the build, so the forking overhead is minimal. By forking memory-intensive compilation into a separate process, we minimize garbage collection in the main Gradle process. Less garbage collection means that Gradle’s infrastructure can run faster, especially when you also use parallel builds.
Forking compilation rarely impacts the performance of small projects. But you should consider it if a single task compiles more than a thousand source files together.
Switch internal-only dependencies to implementation visibility
Only libraries can define api dependencies. Use the
java-library plugin to define API dependencies in your libraries. Projects that use the java plugin cannot declare api dependencies.
|
Before Gradle 3.4, projects declared dependencies using the compile
configuration.
This exposed all of those dependencies to downstream projects. In Gradle 3.4 and above,
you can separate downstream-facing api
dependencies from internal-only implementation
details.
Implementation dependencies don’t leak into the compile classpath of downstream projects.
When implementation details change, Gradle only recompiles api
dependencies.
dependencies {
api(project("my-utils"))
implementation("com.google.guava:guava:21.0")
}
dependencies {
api project('my-utils')
implementation 'com.google.guava:guava:21.0'
}
This can significantly reduce the "ripple" of recompilations caused by a single change in large multi-project builds.
Improve the performance of older Gradle releases
Some projects cannot easily upgrade to a current Gradle version. While you should always upgrade Gradle to a recent version when possible, we recognize that it isn’t always feasible for certain niche situations. In those select cases, check out these recommendations to optimize older versions of Gradle.
Enable the Daemon
Gradle 3.0 and above enable the Daemon by default. If you are using an older version, you should update to the latest version of Gradle. If you cannot update your Gradle version, you can enable the Daemon manually.
Use incremental compilation
Gradle can analyze dependencies down to the individual class level
to recompile only the classes affected by a change.
Gradle 4.10 and above enable incremental compilation by default.
To enable incremental compilation by default in older Gradle versions, add the following setting to your
build.gradle
file:
tasks.withType<JavaCompile>().configureEach {
options.isIncremental = true
}
tasks.withType(JavaCompile).configureEach {
options.incremental = true
}
Use compile avoidance
Often, updates only change internal implementation details of your code, like the body of a method. These updates are known as ABI-compatible changes: they have no impact on the binary interface of your project. In Gradle 3.4 and above, ABI-compatible changes no longer trigger recompiles of downstream projects. This especially improves build times in large multi-project builds with deep dependency chains.
Upgrade to a Gradle version above 3.4 to benefit from compile avoidance.
If you use annotation processors, you need to explicitly declare them in order for compilation avoidance to work. To learn more, check out the compile avoidance documentation. |
Optimize Android projects
Everything on this page applies to Android builds, since Android builds use Gradle. Yet Android introduces unique opportunities for optimization. For more information, check out the Android team performance guide. You can also watch the accompanying talk from Google IO 2017.