Best Practices for Performance
Use UTF-8 File Encoding
Set UTF-8
as the default file encoding to ensure consistent behavior across platforms.
Explanation
Use UTF-8
as the default file encoding to ensure consistent behavior across environments and avoid caching issues caused by platform-dependent default encodings.
This is especially important when working with build caching, since differences in file encoding between environments can cause unexpected cache misses.
To enforce UTF-8
encoding, add the following to your gradle.properties
file:
org.gradle.jvmargs=-Dfile.encoding=UTF-8
Do not rely on the default encoding of the underlying JVM or operating system, as this may differ between environments and lead to inconsistent behavior. |
Use the Build Cache
Use the Build Cache to save time by reusing outputs produced by previous builds.
Explanation
The Build Cache avoids re-executing tasks when their inputs haven’t changed by reusing outputs from previous builds.
This prevents redundant work. If the inputs are the same, the outputs will be too, resulting in faster, more efficient builds.
Example
Don’t Do This
Build caching is disabled by default:
# caching is off by default
# org.gradle.caching=false
Do This Instead
To enable the Build Cache, add the following to your gradle.properties
file:
org.gradle.caching=true
When you build your project for the first time, Gradle populates the cache with the outputs of tasks like compilation.
Even if you run ./gradlew clean
to delete the build directory, Gradle can reuse cached outputs in subsequent builds.
> ./gradlew clean :clean BUILD SUCCESSFUL
On subsequent builds, instead of executing the :compileJava
task again, the outputs of the task will be loaded from the Build Cache:
> ./gradlew compileJava > Task :compileJava FROM-CACHE BUILD SUCCESSFUL in 0s 1 actionable task: 1 from cache
Use the Configuration Cache
Use the Configuration Cache to significantly improve build performance by caching the result of the configuration phase and reusing it in subsequent builds.
Explanation
The Configuration Cache works by saving the result of the configuration phase. On the next build, if nothing relevant has changed, Gradle skips configuration entirely and loads the cached task graph from disk, jumping straight to task execution.
This can dramatically reduce build time for large builds, but it’s just as valuable for smaller builds where configuration overhead can dominate short iterations. Faster feedback helps developers stay focused, without waiting on redundant configuration work.
It’s important to understand how this differs from the Build Cache. The Build Cache stores the outputs of task execution, while the Configuration Cache stores the configured task graph before execution begins. These are independent mechanisms that solve different problems, but they are designed to work together for optimal performance.
The Configuration Cache is the preferred way to execute Gradle builds, but it is not enabled by default. Many existing builds and plugins are not yet fully compatible, and adopting it may involve refactoring of build logic. Enabling it by default could lead to unexpected build failures, so Gradle uses an opt-in adoption model to allow teams to verify compatibility and adopt configuration caching incrementally and safely. |
Example
Don’t Do This
Configuration Caching is not enabled by default:
# caching is off by default
# org.gradle.configuration-cache=false
Do This Instead
To enable the Configuration Cache, add the following to your gradle.properties
file:
org.gradle.configuration-cache=true
When you build your project for the first time, Gradle stores the outcome of the configuration phase, including the task graph, in the Configuration Cache.
> ./gradlew compileJava Configuration cache entry stored. > Task :processResources NO-SOURCE > Task :processTestResources NO-SOURCE > Task :compileJava > Task :classes > Task :compileTestJava NO-SOURCE > Task :testClasses UP-TO-DATE > Task :test NO-SOURCE > Task :check UP-TO-DATE > Task :jar > Task :assemble > Task :build BUILD SUCCESSFUL in 0s 2 actionable tasks: 2 executed
On subsequent builds, instead of reconfiguring tasks like :compileJava
, Gradle loads the task graph from the Configuration Cache and proceeds directly to execution.
> ./gradlew compileJava Configuration cache entry reused. > Task :processResources NO-SOURCE > Task :processTestResources NO-SOURCE > Task :compileJava > Task :classes > Task :compileTestJava NO-SOURCE > Task :testClasses UP-TO-DATE > Task :test NO-SOURCE > Task :check UP-TO-DATE > Task :jar > Task :assemble > Task :build BUILD SUCCESSFUL in 0s 2 actionable tasks: 2 executed
Avoid Expensive Computations in Configuration Phase
Avoid expensive computations in the configuration phase, instead, move them to task actions.
Explanation
In order for Gradle to execute tasks it first needs to build the project task graph. As part of discovering what tasks to include in the task graph, Gradle will configure all the tasks that are directly requested, any task dependencies of the requested tasks, and also any tasks that are not lazily registered. This work is done in the configuration phase.
Performing expensive or slow operations such as file or network I/O, or CPU-heavy calculations in the configuration phase forces these to run even when they might be unnecessary to complete the requested work of the invoked tasks. It is better to move these operations to task actions so that they run only when required.
Example
Don’t Do This
abstract class MyTask : DefaultTask() {
@get:Input
lateinit var computationResult: String
@TaskAction
fun run() {
logger.lifecycle(computationResult)
}
}
fun heavyWork(): String {
println("Start heavy work")
Thread.sleep(5000)
println("Finish heavy work")
return "Heavy computation result"
}
tasks.register<MyTask>("myTask") {
computationResult = heavyWork() (1)
}
abstract class MyTask extends DefaultTask {
@Input
String computationResult
@TaskAction
void run() {
logger.lifecycle(computationResult)
}
}
String heavyWork() {
logger.lifecycle("Start heavy work")
Thread.sleep(5000)
logger.lifecycle("Finish heavy work")
return "Heavy computation result"
}
tasks.register("myTask", MyTask) {
computationResult = heavyWork() (1)
}
1 | Performing heavy computation during configuration phase. |
Do This Instead
abstract class MyTask : DefaultTask() {
@TaskAction
fun run() {
logger.lifecycle(heavyWork()) (1)
}
fun heavyWork(): String {
logger.lifecycle("Start heavy work")
Thread.sleep(5000)
logger.lifecycle("Finish heavy work")
return "Heavy computation result"
}
}
tasks.register<MyTask>("myTask")
abstract class MyTask extends DefaultTask {
@TaskAction
void run() {
logger.lifecycle(heavyWork()) (1)
}
String heavyWork() {
logger.lifecycle("Start heavy work")
Thread.sleep(5000)
logger.lifecycle("Finish heavy work")
return "Heavy computation result"
}
}
tasks.register("myTask", MyTask)
1 | Performing heavy computation during execution phase in a task action. |