Cutting a 9-minute Gradle build to 90 seconds
The three changes that did 90% of the work, and the profiling that pointed to them.
Our CI pipeline had quietly crept up to nine minutes. Here’s how three changes — and a lot of profiling — got it under ninety seconds without touching a line of application logic.
First rule: measure, don’t guess
Before changing anything, I ran the build with --scan and read the timeline. Nine minutes sounds like “the whole thing is slow,” but the scan told a different story: two thirds of the wall-clock time lived in three tasks, and the rest was already fine.
That’s the pattern almost every time. Optimization is a search problem, and the profiler is the map. Guessing just moves work around.
Change 1 — parallelize the archiving
The single biggest culprit was packaging: zipping thousands of class files into jars, single-threaded, on a 16-core runner. So I wrote a plugin that fans the archiving out across cores while keeping the output byte-for-byte reproducible.
// build.gradle.kts
plugins {
id("io.github.kukis13.parallel-zip") version "1.2.0"
}
tasks.withType<Zip>().configureEach {
parallel = true // use all cores, reproducible output
}
Zip of 512 modules: 1.38s → 0.39s (3.5×). On the war task it was closer to 11×.
Change 2 — fix the task graph
A misconfigured dependency was forcing a full recompile of every module whenever one changed. One line in the convention plugin restored incremental compilation and Gradle’s build cache started actually hitting.
Change 3 — stop doing work twice
A code-generation step ran on every build even when its inputs hadn’t changed, because its outputs weren’t declared. Wiring up proper task inputs/outputs let Gradle skip it entirely on cache hits.
Add it up and the median CI build dropped from nine minutes to about ninety seconds. The best part: none of it touched product code — just the machinery around it.