A fast, reliable CI pipeline is the foundation of a productive Android team. The biggest wins come from smart Gradle caching and parallelism — not from faster machines.
Gradle Build Performance Fundamentals
Understand the Build Phases
initialization → Configuration → Task graph → Execution
- Configuration:
build.gradle.ktsevaluated for all projects — avoid I/O or network calls here - Execution: only tasks that are not up-to-date run — cache maximizes this benefit
Enable Build Caching
// gradle.properties
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.daemon=false // false in CI; daemon is for local dev
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
// Enable configuration cache (Gradle 8+)
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
Remote Build Cache (GitHub Actions)
# .github/workflows/ci.yml
- name: Gradle cache
uses: gradle/gradle-build-action@v2
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# Write cache only on main; PRs use read-only (saves cache quota)
- name: Build & test
run: ./gradlew assembleDebug testDebugUnitTest --build-cache
Gradle Home Cache (Dependencies)
Cache the Gradle wrapper and dependencies separately from the build output cache:
- name: Setup Java
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle' # caches ~/.gradle/caches and ~/.gradle/wrapper
Android-Specific Caching Targets
- name: Cache Gradle outputs
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.android/build-cache
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
Parallelism: Module-Level
Split your CI into parallel jobs by test type:
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- run: ./gradlew testDebugUnitTest
lint:
runs-on: ubuntu-latest
steps:
- run: ./gradlew lintDebug
instrumented-tests:
runs-on: ubuntu-latest
steps:
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
script: ./gradlew connectedDebugAndroidTest
Build Scan: Diagnosing Slow Builds
// settings.gradle.kts
plugins {
id("com.gradle.enterprise") version "3.16.2"
}
gradleEnterprise {
buildScan {
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"
publishAlways()
}
}
Build scans show task execution timeline, cache hit/miss rates, and which tasks are the bottleneck.
Common Speed Gains
| Optimization | Typical Savings |
|---|---|
| Gradle build cache (local) | 30–60% on incremental builds |
| Remote cache (CI) | 40–70% on PR builds |
| Parallel modules | Linear with module count |
| Configuration cache | 10–30% on non-incremental |
kapt → KSP migration | 20–40% on annotation processing |
Avoid api() over implementation() | Fewer downstream recompilations |
Key Takeaways
| Rule | Why |
|---|---|
org.gradle.caching=true | Reuse outputs from previous builds |
cache: 'gradle' in setup-java | Caches the dependency download step |
| Write cache only on main branch | PR builds get read-only; avoids cache poisoning |
| Parallelize by job type | Unit tests, lint, and instrumented tests run concurrently |
| Use Build Scans to diagnose | You can't optimize what you can't measure |