| name | build-time-optimization |
| description | Cut local and CI build times on Android (Gradle config cache, build cache, KSP over KAPT) and iOS (Xcode build cache, ccache, modules). Use when clean builds exceed ten minutes. |
Build Time Optimization
Instructions
Slow builds kill iteration. On mobile, a 10-minute CI cycle becomes 40 minutes when flakes retry. This skill covers the high-impact settings for Android (Gradle) and iOS (Xcode) plus Flutter and React Native.
1. Measure First
Android:
./gradlew assembleDebug --scan
Publish the build scan — it highlights longest tasks, non-cacheable steps, configuration time, and up-to-date hit rates. Without the data, tweaking Gradle is guessing.
Xcode: Product → Perform Action → Build with Timing Summary. Or:
xcodebuild -project App.xcodeproj -scheme App -showBuildTimingSummary clean build
Look at the largest Compile/CompileSwift/Ld times.
2. Android — Gradle Configuration Cache
gradle.properties:
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8
kotlin.incremental.useClasspathSnapshot=true
android.enableR8.fullMode=true
The configuration cache alone typically halves incremental configuration time. It is strict — kill code that reads System env at configuration time or uses deprecated Groovy closures.
3. Android — KSP over KAPT
KAPT stubs Java for annotation processing and is the #1 hot spot after build tools. Migrate to KSP where available (Room, Dagger/Hilt, Moshi).
plugins {
id("com.google.devtools.ksp") version "2.0.0-1.0.21"
}
dependencies {
ksp("com.google.dagger:hilt-compiler:2.51")
ksp("androidx.room:room-compiler:2.6.1")
}
Typical win: 30–50% faster annotation processing, 2–4× faster incremental.
4. Android — Modularization
Gradle caches per-module. A 50-module project rebuilds only dirty modules. Rules:
- Feature modules should not depend on each other; they depend on a thin
:core and on interfaces exposed by :data.
- Avoid
api dependencies on leaf modules; use implementation so changes do not ripple.
- Use Version Catalogs (
libs.versions.toml) — faster evaluation, single source of truth.
5. Android — Build Cache (Remote)
For team and CI, share the build cache:
# gradle.properties
org.gradle.caching=true
buildCache {
local { isEnabled = true }
remote<HttpBuildCache> {
url = uri("https://cache.ci.example.com/cache/")
isPush = System.getenv("CI") == "true"
credentials {
username = System.getenv("BUILD_CACHE_USER")
password = System.getenv("BUILD_CACHE_PASS")
}
}
}
Remote cache hit rates above 70% turn CI times into seconds.
6. iOS — Xcode Tuning
- Enable Build Timing Summary persistently: Defaults write
ShowBuildOperationDuration to YES.
- New Build System is the default on modern Xcode; ensure no legacy settings linger.
- Turn on User Script Sandboxing only for scripts that comply; otherwise it disables parallelism.
- Avoid
find . -name scripts in build phases — they disable remote caching.
Module discipline for Swift:
- Prefer static frameworks over dynamic for first-party code — faster link.
- Keep modules small;
import of a 50k-LOC module is expensive.
- Use
@_spi / internal boundaries to reduce public surface that recompiles consumers.
- Avoid default protocol witness synthesis in hot code paths; explicit implementations compile faster.
7. iOS — ccache for Objective-C/C/C++
If your project has substantial ObjC/C++ (e.g., CocoaPods with C++ pods, Flutter plugins):
brew install ccache
ln -sf $(brew --prefix)/bin/ccache /usr/local/bin/clang
ln -sf $(brew --prefix)/bin/ccache /usr/local/bin/clang++
Typical win: 30–60% faster on hot rebuilds.
For Swift, ccache does not help. Use Xcode's built-in incremental compiler and avoid forced clean builds.
8. iOS — CocoaPods & SPM
- Prefer Swift Package Manager over CocoaPods where possible — SPM is faster and cache-friendly.
- If stuck on Pods, enable
use_modular_headers! and COCOAPODS_PARALLEL_CODE_SIGN.
- Pre-build binary
.xcframework for big third-party libs; ship as SPM binary target.
9. Flutter
flutter build apk --split-per-abi builds only the requested ABI.
- Commit
.dart_tool/package_config.json? No — but do commit pubspec.lock.
- Turn on Gradle config cache just like a native Android project (above). Flutter passes it through.
- Use
pub get with a private mirror for CI if pub.dev is a bottleneck.
10. React Native
- Metro cache: ensure CI restores
node_modules/.cache/metro.
- Hermes bytecode cache on CI via
hermesc precompilation for release.
- Yarn / pnpm with workspace-aware cache. pnpm's content-addressable store is very CI-friendly.
- EAS Build cache or Turborepo for monorepos — cache-key on package hashes.
11. CI Caches
Cache on key = lockfile hash:
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
iOS Derived Data:
- uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: xcode-${{ hashFiles('**/Package.resolved','**/Podfile.lock') }}
12. Track Regressions
Every PR should record build time for a cold and a warm build on a stable runner. Fail if median builds regress > 15%.
Checklist