At PT Luna Aplikasi Indonesia, our Android CI pipeline was taking 15+ minutes per build. PRs were slow to merge and developers lost focus context switching while waiting. Here's how I cut that to under 12 minutes.
Step 1: Profile Before Optimizing
First, I timed each phase. Turns out 60% of time was spent on Gradle dependency downloads and compilation. Caching was either absent or misconfigured.
Step 2: Aggressive Dependency Caching
This single change cut 3–4 minutes per build. The key is caching both the Gradle wrapper and the actual dependency cache, keyed to your lockfile.
- name: Cache Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
Step 3: Parallel Jobs
Split unit tests, lint, and build into parallel jobs. Unit tests and lint don't depend on the build artifact — they can run concurrently.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: ./gradlew lint
test:
runs-on: ubuntu-latest
steps:
- run: ./gradlew test
build:
runs-on: ubuntu-latest
needs: [lint, test] # Gate release on quality checks
steps:
- run: ./gradlew assembleRelease
Step 4: Build Configuration
- Enable Gradle daemon and parallel execution in gradle.properties
- Set org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC for faster compilation
- Use --build-cache flag to reuse outputs from previous builds
💡 Final result: 15:20 → 11:48 average build time (23% reduction). Extrapolated across the team's ~20 daily builds, that's ~1 hour of CI time saved per day.
Conclusion
CI optimization is high-leverage work — a few hours of setup pays dividends for the lifetime of the project. Start with caching, then parallelize, then profile remaining bottlenecks.