Cutting Gradle Configuration Time in Half with Lazy Task Registration
Learn how to use Gradle's configuration avoidance to cut build startup time. Replace tasks.create with tasks.register to defer task creation and improve performance in multi-project builds.
31 Jul 2026, 21:33 UTC

The Problem: Configuration Phase Bottlenecks
In a 50-module Gradle build, the configuration phase alone can take 30–60 seconds before any task executes. Every tasks.create, every plugin applied eagerly, and every property accessed during configuration forces Gradle to instantiate and configure objects that may never be used in the current invocation. For developers running ./gradlew test ten times a day, that is minutes of wasted time per session.
How Configuration Avoidance Works
Gradle's configuration avoidance defers object creation until the task is actually needed. Instead of tasks.create(\"myTask\"), which immediately configures the task, you use tasks.register(\"myTask\"). This returns a TaskProvider—a lazy reference. The task is only realized when something calls get() on that provider, or when the task graph is calculated for execution.
The same pattern applies to properties: Property<String> and Provider<File> hold values lazily. This ensures that expensive computations, such as resolving a dependency configuration, only run when the value is consumed by a task during the execution phase.
Worked Example: Migrating a Custom Task
Consider a task that generates documentation. In an eager setup, the task is created every time any Gradle command is run, regardless of whether the documentation is actually being generated.
Before (Eager Configuration):
// build.gradle.kts
val myTask = tasks.create(\"generateDocs\") {
group = \"documentation\"
doLast {
val output = file(\"build/docs\")
output.mkdirs()
output.resolve(\"index.html\").writeText(\"Hello\")
}
}
// Another task depends on it
tasks.named(\"compileKotlin\") {
dependsOn(myTask) // forces eager configuration
}
After (Lazy Configuration):
// build.gradle.kts
val myTask = tasks.register(\"generateDocs\") {
group = \"documentation\"
doLast {
val output = file(\"build/docs\")
output.mkdirs()
output.resolve(\"index.html\").writeText(\"Hello\")
}
}
// Use provider-based dependency
tasks.named(\"compileKotlin\") {
dependsOn(myTask) // TaskProvider, no eager realization
}
To implement this, run the following commands in your terminal at the project root (requires write access to build.gradle.kts):
# 1. Establish baseline profile
./gradlew help --profile
# 2. Apply the lazy registration changes
# 3. Compare configuration time
./gradlew help --profile
Open build/reports/profile/profile-<timestamp>.html and compare the "Configuration" row. In large multi-project builds, a 30–50% reduction in this duration is typical.
Trade-offs and Gotchas
- Mixing eager and lazy breaks avoidance: Accessing
myTask.get().outputsduring the configuration phase realizes the task immediately, defeating the purpose ofregister. UsemyTask.configure { ... }ormyTask.map { it.outputs }to maintain laziness. - Custom plugin updates: Plugins that call
project.tasks.createor access task properties insideapply()will trigger eager configuration. These must be refactored to usetasks.registerand lazy property APIs. - DSL Differences: While both Groovy and Kotlin DSLs support these features, the Kotlin DSL provides stronger compile-time guarantees for lazy types, making it easier to spot accidental eager access.
Verify the Result
- Use the
--profileflag to compare configuration duration before and after the migration. - Run
./gradlew -d --infoand monitor the logs. A successful implementation will show fewer "Configuring task" messages during the configuration phase. - Run
./gradlew tasks --all. Your registered tasks should still appear in the list, confirming they are known to Gradle but not yet fully instantiated.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.