Gradle Build Optimization: Cutting Android Build Times in Half

Diagnose slow builds with build scans, then fix with configuration cache, parallel execution, and dependency optimization.

Introduction

Slow builds kill productivity. Developers wait minutes for builds multiple times daily. This guide diagnoses build bottlenecks and implements fixes: configuration cache, dependency optimization, and incremental compilation tuning.

Diagnosing Build Bottlenecks

Use ./gradlew build --scan for build scans. Analyze task execution times. Identify configuration vs execution time. Use --profile for visual breakdown.

Configuration Cache

Enable org.gradle.configuration-cache=true. Fix configuration cache violations. Expect 50-80% reduction for subsequent builds. Cache invalidates on build logic changes.

Parallel Execution

Set org.gradle.parallel=true. Configure project dependencies for parallel builds. Use includeBuild for composite builds. Parallel execution can double build speed.

Dependency Optimization

Use implementation vs api correctly. Avoid transitive dependency bloat. Use dependency substitution for local development. Consider dependency locking for reproducibility.

Frequently Asked Questions

Why is my Gradle configuration so slow?

Common causes: too many plugins, complex build logic in build.gradle, missing configuration cache. Move logic to convention plugins or precompiled script plugins.

How do I speed up dependency resolution?

Use dependency locking. Configure repositories efficiently (remove unused repos). Use Gradle's dependency verification. Consider a local artifact cache like Gradle Enterprise.

SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC07:38:22
UP00:01
MOD · ARTICLE · BUILD TOOLSS/N · AX-GRADLEPUBLISHED
Build ToolsFeb 20, 202611 MIN

Gradle Build Optimization: Cutting Android Build Times in Half

Diagnose slow builds with build scans, then fix them with configuration cache, parallel execution, dependency optimization, and incremental compilation tuning.

By Rocky Elsalaymeh · Founder & Principal Consultant, Strategia-X

Why Build Speed Matters More Than You Think

A 60-second build that runs 30 times a day costs 30 minutes of context-switching per developer. Across a team of 5, that is 12.5 hours per week of lost focus -- not because developers sit idle, but because every build break interrupts flow state. Slower builds also mean longer CI pipelines. When CI takes 20+ minutes, developers batch changes into larger PRs, review becomes harder, and merge conflicts multiply. Cutting build time creates a compounding effect: smaller PRs, faster reviews, fewer conflicts, and more deployments. The good news: most Android projects leave significant performance on the table because Gradle's defaults are conservative. A few configuration changes often cut build time by 30-50%.

Diagnosing With Build Scans and Profile Reports

Never optimize blind. Run a build scan first to see exactly where time is spent. Gradle build scans show configuration time, task execution time, dependency resolution, and cache hit rates. The two most common bottlenecks: configuration time (Gradle evaluates all build scripts even for a single module build) and cache misses (tasks re-execute because inputs changed unexpectedly).
bash
# Generate a Gradle profile report (local)
./gradlew assembleDebug --profile

# Generate a build scan (hosted on scans.gradle.com)
./gradlew assembleDebug --scan

# Common findings in build scans:
# 1. Configuration phase > 5s → too many plugins
#    or dynamic dependency resolution
# 2. Cache miss rate > 30% → unstable task inputs
# 3. A single task taking > 30% of build time
#    → investigate that task specifically
# 4. No parallel execution → missing configuration

# Quick diagnostic: compare clean vs incremental
time ./gradlew clean assembleDebug   # Clean build
time ./gradlew assembleDebug         # Incremental
# If incremental is not significantly faster,
# caching/incrementality is broken

Configuration Cache and Parallel Execution

The configuration cache serializes the task graph after the first build and reuses it on subsequent builds, skipping the entire configuration phase. This alone can save 5-15 seconds per build on medium-sized projects. Parallel execution runs independent tasks and subprojects concurrently. Combined with the configuration cache, these two settings often deliver the largest improvement with zero code changes.
properties
# gradle.properties - Core performance settings

# Enable configuration cache (Gradle 8.1+)
org.gradle.configuration-cache=true
# Show problems as warnings during adoption
org.gradle.configuration-cache.problems=warn

# Parallel execution for multi-module projects
org.gradle.parallel=true

# Gradle daemon keeps JVM warm between builds
org.gradle.daemon=true

# Increase daemon heap for large projects
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError \
  -Dfile.encoding=UTF-8 \
  -XX:MaxMetaspaceSize=512m

# Build cache (local by default)
org.gradle.caching=true

# Non-transitive R classes (AGP 8.0+)
# Prevents R class changes from cascading rebuilds
android.nonTransitiveRClass=true

# Disable unused Android features
android.defaults.buildfeatures.aidl=false
android.defaults.buildfeatures.buildconfig=false
android.defaults.buildfeatures.renderscript=false
android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false

Dependency Management for Faster Resolution

Dynamic dependency versions (e.g., "1.+") force Gradle to check Maven repositories on every build. Pin exact versions. Use a version catalog (libs.versions.toml) to centralize version declarations and avoid resolution overhead. Dependency substitution and force-resolution also slow builds. Audit your dependency graph with ./gradlew dependencies and look for version conflicts that trigger expensive resolution strategies.
toml
# gradle/libs.versions.toml - Centralized version catalog
[versions]
kotlin = "2.1.0"
compose-bom = "2026.01.00"
hilt = "2.51.1"
room = "2.7.0"
ktor = "3.0.3"
coroutines = "1.9.0"

[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }

hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }

room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

[bundles]
compose = ["compose-ui", "compose-material3"]
room = ["room-runtime", "room-ktx"]

[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

Module Structure and Build Avoidance

In a multi-module project, changing a file in :core:model triggers recompilation of every module that depends on it. The key optimization is minimizing the API surface of shared modules so that implementation changes do not break the ABI and trigger downstream recompilation. Use the implementation dependency configuration instead of api wherever possible. implementation dependencies are invisible to consumers, so changes to them do not invalidate downstream caches. Reserve api for types that appear in your module's public signatures.
kotlin
// core/model/build.gradle.kts
// This module's public API is ONLY data classes
// used across features. Keep it tiny.
plugins {
    id("com.android.library")
    alias(libs.plugins.kotlin.android)
}

dependencies {
    // api: exposed to consumers because Task
    // appears in public function signatures
    api(libs.kotlinx.datetime)

    // implementation: internal to this module,
    // changes here don't trigger downstream rebuilds
    implementation(libs.kotlinx.serialization.json)
}

// feature/tasks/build.gradle.kts
dependencies {
    // Only depend on the interface module, not
    // the implementation -- faster builds
    implementation(project(":core:model"))
    implementation(project(":core:data-api"))

    // NOT this: pulls in all of :core:data
    // and its transitive dependencies
    // implementation(project(":core:data"))
}

CI-Specific Optimizations

CI environments have different characteristics than developer machines: no warm Gradle daemon, fresh filesystem, but often more CPU cores. Tailor your CI Gradle configuration accordingly. Enable remote build caching so that CI builds benefit from previous runs. Use Gradle's --no-daemon flag on CI (the daemon provides no benefit for single-shot builds). Set org.gradle.workers.max to match your CI runner's CPU core count for maximum parallelism.
yaml
# .github/workflows/android-ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: zulu
          java-version: 17

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4
        with:
          cache-read-only: ${{ github.ref != 'refs/heads/main' }}

      - name: Build debug APK
        run: >
          ./gradlew assembleDebug
          --no-daemon
          --build-cache
          --parallel
          -Porg.gradle.workers.max=4
          -Porg.gradle.jvmargs="-Xmx4g"

      - name: Run unit tests
        run: >
          ./gradlew testDebugUnitTest
          --no-daemon
          --build-cache
          --parallel

      - name: Upload build reports
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: build-reports
          path: "**/build/reports/"
MOD · TAKEAWAYS6 POINTSSUMMARY

Key Takeaways

  1. 1Run a build scan before optimizing -- never guess where build time is spent.
  2. 2Configuration cache + parallel execution often deliver 30-50% improvement with zero code changes.
  3. 3Pin dependency versions in a version catalog to avoid dynamic resolution overhead.
  4. 4Use implementation instead of api for dependencies to prevent cascading rebuilds.
  5. 5Keep shared module API surfaces small -- only expose types that consumers actually need.
  6. 6Configure CI separately: disable daemon, enable remote cache, maximize worker threads.
MOD · FAQ2 ENTRIESANSWERED

Frequently Asked

Why is my Gradle configuration so slow?

Common causes: too many plugins, complex build logic in build.gradle, missing configuration cache. Move logic to convention plugins or precompiled script plugins.

How do I speed up dependency resolution?

Use dependency locking. Configure repositories efficiently (remove unused repos). Use Gradle's dependency verification. Consider a local artifact cache like Gradle Enterprise.

MOD · BUILD · NEXTS/N · AX-CTA-0001READY

Ready to architect your next Android app?

ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.

ANDROID-ARCHITECT · CONSOLE
S/N · AX-1A-00001
STRATEGIA-X · ENGINEERED · IN · CALIFORNIA