Skip to content
All posts
August 28, 20268 min read

Speeding Up Gradle Builds: What Actually Made a Difference

After 13 years of Android builds and 22 shipped apps, here are the Gradle optimizations that genuinely cut my build times — not theoretical advice, just the knobs I actually turned and the results I measured.

AndroidPerformanceTools
Share:

I shipped my first Android app in 2012, when Gradle was still the new kid on the block and clean builds took a coffee break. Thirteen years and 22 apps later, I have spent more hours than I care to admit staring at a spinner. Most "speed up your Gradle build" posts recycle the same five tips. This one is different. I am only writing about the changes that actually moved the needle on real projects, with the numbers I measured.

If you maintain a medium to large Android codebase and your CI feels like a tax, this is the playbook.

The Baseline: What Slows a Gradle Build Down

Before tuning anything, I profiled. Gradle's

code
--profile
flag dumps a report you can open in any browser. It shows exactly where time is going: configuration, dependency resolution, task execution, and the long tail of
code
:app:processDebugResources
style work.

On my current project, a single-module app with around 90k lines of Kotlin, the baseline numbers looked like this:

Build typeCold (no cache)Warm (incremental)
Debug assemble4m 12s38s
Release assemble5m 48s51s
Unit tests2m 30s22s
Lint3m 10s1m 05s

Two things jumped out. First, KAPT was eating a huge slice of configuration time. Second, every clean build downloaded and processed the same dependencies from scratch, even when nothing in

code
build.gradle.kts
had changed. That pointed at two real targets.

[!NOTE] Always run with

code
--profile
before optimizing. Without data, you are guessing. Guessing with Gradle is a great way to spend a day making things worse.

The Changes That Actually Helped

I tried roughly a dozen tweaks over six months. Five made a measurable difference. The rest were either neutral, already enabled, or actively harmful. Here is what worked, in order of impact.

1. Migrating off KAPT to KSP

KAPT runs the Java annotation processor pipeline for every Kotlin file. It is slow, it duplicates work, and it has been officially discouraged for years. KSP does the same job by reading Kotlin's own AST directly.

The migration for my Hilt and Room setup took an afternoon. The configuration change:

kotlin
// build.gradle.kts (project)
plugins {
    id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}

// build.gradle.kts (app)
plugins {
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("androidx.room:room-runtime:2.6.1")
    ksp("androidx.room:room-compiler:2.6.1")
    implementation("com.google.dagger:hilt-android:2.51.1")
    ksp("com.google.dagger:hilt-android-compiler:2.51.1")

    // Replace any kapt(...) lines with ksp(...)
}

[!IMPORTANT] If you still use kapt anywhere, the build pays a tax on every single Kotlin compilation. The KSP migration was the single biggest win I have ever seen on Android build performance.

Cold build dropped from 4m 12s to 3m 04s on the same machine. Configuration time alone fell by roughly 40 percent.

2. Enabling the Build Cache Properly

Gradle's build cache stores task outputs by their inputs. Reuse them and you skip work that has already been done. The trick is making sure the cache actually has a home and CI knows about it.

Locally, I set a fixed cache directory so different projects stop fighting over the default location. On CI, I point at a remote cache backed by object storage.

kotlin
// settings.gradle.kts
pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

// Enable build cache and configure remote cache for CI
buildCache {
    local {
        directory = File(System.getProperty("user.home"), ".gradle/caches/build-cache")
        removeUnusedEntriesAfterDays = 30
    }
    remote(HttpBuildCache::class.java) {
        url = uri("https://cache.example.com/gradle-cache/")
        isAllowInsecureProtocol = false
        // Push only from CI to avoid local machines spamming the remote
        isPush = System.getenv("CI") != null
        credentials {
            username = System.getenv("CACHE_USER") ?: ""
            password = System.getenv("CACHE_TOKEN") ?: ""
        }
    }
}

On CI, warm build times fell from around 90 seconds to under 25 seconds because the same Maven artifacts and intermediate outputs from the previous run were reused.

[!TIP] Use a separate

code
~/.gradle/caches/build-cache
directory per project if you work on several. The default cache mixes projects, and a corrupted entry from one repo will poison another.

3. Splitting the App Module by Feature

For years my projects were single-module. It is the path of least resistance, and the path of slowest builds. Every change recompiled the entire codebase.

I split the project into a

code
:core
module for shared utilities, feature modules for each major screen group, and a thin
code
:app
module that wires them together. The module structure:

kotlin
// settings.gradle.kts
include(":app")
include(":core")
include(":core:network")
include(":core:database")
include(":feature:home")
include(":feature:settings")
include(":feature:billing")

// feature:home/build.gradle.kts
plugins {
    id("com.android.library")
    id("org.jetbrains.kotlin.android")
    id("com.google.devtools.ksp")
}

android {
    namespace = "com.sudarshantechlabs.feature.home"
    compileSdk = 35
    defaultConfig {
        minSdk = 24
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
}

dependencies {
    implementation(project(":core"))
    implementation(project(":core:database"))

    implementation("androidx.compose.ui:ui:1.7.5")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
    implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
}

The first time I touched only

code
feature:home
, the incremental build dropped to under 10 seconds. Gradle simply had no reason to recompile
code
feature:billing
or
code
core:network
. Local developer loop went from "I have time to read a Slack thread" to "the build finished before I switched windows."

[!WARNING] Module splits have a real cost. Every

code
internal
symbol you want to share has to either become
code
public
or move to a
code
:core
module. Decide before you start splitting. A messy split is worse than a slow single module.

4. Disabling Unnecessary Build Features

Some features look free and are not. In every project I run, I now disable three things by default: BuildConfig generation, vector drawable rasterization for old API levels, and the resource shrinker on debug builds.

kotlin
// app/build.gradle.kts
android {
    buildFeatures {
        buildConfig = false
        // Only enable when you actually use it
        // aidservice = true
    }

    defaultConfig {
        // minSdk is 24 in my projects, so PNG rasterization is dead weight
        vectorDrawables {
            useSupportLibrary = false
        }
    }

    buildTypes {
        debug {
            isMinifyEnabled = false
            isShrinkResources = false
        }
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

code
buildConfig = false
alone saved around four seconds per build, and resource shrinking on debug was a silent killer that added 15 to 20 seconds with zero benefit for development.

5. Parallel Execution and Configuration on Demand

Two Gradle properties do most of the heavy lifting. Add them to

code
gradle.properties
:

properties
# gradle.properties
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+UseParallelGC
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true

code
org.gradle.parallel=true
lets independent project tasks run on multiple cores.
code
org.gradle.configureondemand=true
skips configuration of modules Gradle knows it will not need. Together, on a 12-core machine, cold builds dropped by another 20 to 25 percent.

[!TIP] If you use Android Studio, also bump

code
-Xmx
to at least 4 GB. Default heap allocation is 512 MB, which forces frequent GC pauses and is a leading cause of "the IDE is sluggish" complaints that are actually Gradle complaints.

What Did Not Help

I want to be honest about the things that did not matter.

Switching from Groovy to Kotlin DSL in

code
build.gradle
files was a wash. I prefer Kotlin DSL for type safety, but my build times did not change.

Removing

code
apply plugin
calls and moving them to a
code
plugins {}
block is correct hygiene, but the performance difference is in the noise.

Adding

code
--offline
mode helps CI in flaky network conditions but does not change local builds.

Generating sources once via

code
ksp { arg("room.schemaLocation", ...)}
is great for migrations, not for speed.

Where I Landed

After all changes, the same project looks like this:

Build typeBeforeAfterImprovement
Debug assemble (cold)4m 12s2m 18s45 percent faster
Debug assemble (warm)38s9s76 percent faster
Release assemble (cold)5m 48s3m 30s40 percent faster
Unit tests (cold)2m 30s1m 18s48 percent faster
Lint3m 10s1m 42s46 percent faster

The warm build number is what changed my day-to-day life. Sub-10-second builds on a medium-sized app means I stay in flow. I do not context-switch. I do not check Slack while waiting. That is worth more than any of the headline cold-build savings.

Key Takeaways

  • Migrate from KAPT to KSP today. It is the single largest Android build performance win available right now, and it is a one-afternoon change for most projects.
  • Set up a real build cache, both local and remote. Use a dedicated
    code
    ~/.gradle/caches/build-cache
    directory per project, and only push to the remote cache from CI.
  • Split your app into feature modules before you split anything else. Incremental builds of one feature dropping to under 10 seconds is what actually changes how you work.
  • Disable
    code
    buildConfig
    , debug resource shrinking, and old vector rasterization.
    Each costs seconds per build with no development benefit.
  • Tune
    code
    gradle.properties
    last.
    code
    org.gradle.parallel=true
    ,
    code
    org.gradle.caching=true
    , and
    code
    -Xmx4g
    are mandatory, but they are finishing moves, not opening moves.

The boring truth is that build performance is a stack. The base layer is what Gradle is asked to do, and that is shaped by your build files. The middle layer is parallelism and caching. The top layer is your machine. Fix the base first. Everything else is decoration.

Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus