On a typical GitHub Actions + Xcode iOS project, slow iOS CI build times are often treated as inevitable. They are not. The real issue is that iOS CI slow ≠ Xcode slow — the pipeline is stateless, uncached, and serial by design.
This post walks through a real 8-person iOS team (460K lines of Swift, 28 Pods, 14 SPM packages, 30+ pushes per day) that cut GitHub Actions macOS CI from 28 minutes to 9, with a complete iOS CI optimization playbook you can copy.
1. Problem definition: why is iOS CI slow?
Most teams accept 20–30 minute feedback loops as normal. The root cause is not Xcode — it is how the CI pipeline is structured.
1.1 The core issue with GitHub Actions CI
GitHub Actions macOS runners are an ephemeral environment. Every run starts from scratch, which means:
- ❌ DerivedData is wiped (no incremental build)
- ❌ CocoaPods runs a full install every time
- ❌ SPM resolves from scratch every time
- ❌ Build context cannot be reused
Result: every CI run is a cold build.
1.2 Baseline build breakdown (28 minutes)
We triggered cold runs via workflow_dispatch three times and took the median. Stage-by-stage timing:
| Stage | Time | Type |
|---|---|---|
| Checkout | 45s | Fixed |
| pod install | 3m 12s | Network |
| SPM resolve | 1m 44s | Resolution |
| xcodebuild build | 11m 08s | Cold compile |
| xcodebuild test | 6m 22s | CPU |
| xcodebuild archive | 5m 30s | Serial blocker |
| upload/sign | 1m 05s | Fixed |
| Total | 29m 46s |
Core bottlenecks: three reasons iOS CI is slow
- Cold build (39%) — no DerivedData, so 460K lines of Swift recompile every run
- Dependency re-resolve (17%) — Pods and SPM re-download and re-parse on every run
- Serial pipeline (19%) — test and archive have no dependency yet run one after the other
2. Optimization goals and results
Results at a glance (iOS CI optimization)
| Stage | Build time |
|---|---|
| baseline | 28 min |
| + cache | 20 min |
| + parallel | 12 min |
| + Apple Silicon | 9 min |
Where the savings came from
- Cache: 42% (~8 minutes)
- Parallelization: 32% (~6 minutes)
- Apple Silicon: 26% (~3 minutes)
Case study context (E-E-A-T)
| Parameter | Value |
|---|---|
| Swift LOC | ~460K (including tests) |
| CocoaPods / SPM | 28 Pods + 14 packages |
| Baseline runner | GitHub macos-latest (Intel, 4 cores) |
| Team / cadence | 8 engineers, 30+ pushes/day |
| Measurement | Timestamp each step; cold-run median of 3 |
Single-variable experiments (10 runs per config, median): cache alone −29%, parallelization alone −21%, Apple Silicon alone −18%. Structural changes (cache + parallel) delivered 74% of the gain; hardware raises the ceiling, not the floor. p95 dropped from 33 min to 12 min; DerivedData cache hit rate reached 80%.
3. Optimization #1: CI caching (biggest win)
3.1 Why caching matters most
The heart of iOS CI optimization is making Xcode incremental builds work again in CI. That requires persisting three directories: DerivedData, CocoaPods cache, and Swift Package Manager cache.
3.2 The three cache targets
- DerivedData:
~/Library/Developer/Xcode/DerivedData - CocoaPods cache:
~/Library/Caches/CocoaPods - SPM cache:
~/.spm-cache
3.3 GitHub Actions configuration
- name: Cache DerivedData
uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: deriveddata-${{ runner.os }}-${{ hashFiles('**/Podfile.lock','**/Package.resolved') }}
- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: ~/Library/Caches/CocoaPods
key: pods-${{ runner.os }}-${{ hashFiles('**/Podfile.lock') }}
- name: Cache SPM
uses: actions/cache@v4
with:
path: ~/.spm-cache
key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}
3.4 Cache key design
Hit rate determines whether your DerivedData cache actually pays off. Three common key strategies:
| Key strategy | Hit rate | Issue |
|---|---|---|
runner.os only | ~100% | May restore stale Pods; inconsistent artifacts |
| Lockfile hash (recommended) | ~80% | Hits when deps unchanged; rebuilds only on update |
| Includes commit SHA | ~0% | Every push misses — effectively no cache |
Hash Podfile.lock and Package.resolved as the primary key, and add restore-keys prefix fallbacks so a primary miss still restores the last good cache and triggers incremental compile instead of a full cold build. In CI, always run pod install --no-repo-update to avoid rewriting lockfiles and busting the cache.
See the GitHub Actions cache documentation; for queue and runner bottlenecks that cache alone cannot fix, read why GitHub Actions macOS runners always queue.
3.5 Cache impact
| Step | Before | After |
|---|---|---|
| pod install | 3m 12s | 50s |
| SPM resolve | 1m 44s | 15s |
| build | 11m | 3–4m |
Average savings: 8–10 minutes.
4. Optimization #2: CI parallelization (pipeline optimization)
4.1 The problem
Default CI runs build → test → archive serially. But test and archive do not depend on each other — they can run in parallel.
4.2 Parallel job design
jobs:
build-and-test:
runs-on: macos
steps:
- run: xcodebuild build test
archive:
runs-on: macos
if: github.ref == 'refs/heads/main'
steps:
- run: xcodebuild archive
Archive runs only on main; each job restores its own cache. Manage signing with Fastlane Match. For resource isolation at scale, see how 3 Cloud Macs handle 500 iOS CI builds per day.
4.3 Impact
| Mode | Time |
|---|---|
| serial | 20 min |
| parallel | 12 min |
Saves 5–7 minutes.
5. Optimization #3: Apple Silicon runners
5.1 Why this comes last
Hardware is not the first lever for slow iOS CI. Without caching, switching to Apple Silicon saves only ~18%. Apply structural fixes first, then upgrade hardware to go from 12 min down to 9.
5.2 Measured gains (cache + parallel already enabled)
| Stage | Intel | Apple Silicon |
|---|---|---|
| build | 3m 40s | 1m 55s |
| test | 6m 22s | 3m 48s |
| archive | 5m 30s | 3m 10s |
5.3 Takeaway
Additional savings: 3–5 minutes. Apple Silicon helps most during Swift compile and link phases; see Apple's incremental build guide.
5.4 Three macOS runner options compared
After structural optimization, most teams choose among three GitHub Actions self-hosted macOS runner paths:
| Option | Build time | Queue | Ops overhead |
|---|---|---|---|
| GitHub macos-latest | ~20 min | High (10–20 min at peak) | None |
| Self-hosted Mac mini (Intel) | ~14 min | None | High |
| Cloud Mac (Apple Silicon) | ~9 min | None | Low |
GitHub-hosted runners are free but queue during peaks, and cache storage is capped at 10 GB per repo — large projects evict frequently. Self-hosted eliminates queues but Intel hardware hits a ceiling. For teams pushing 30+ times a day, a dedicated Apple Silicon node usually has the best ROI: fix the pipeline yourself, then evaluate hardware and queue separately.
6. iOS CI optimization decision tree
CI > 15 min?
↓
cold build > 40%?
→ YES: enable cache (DerivedData + Pods + SPM)
↓
pipeline serial?
→ YES: split jobs (test / archive)
↓
still slow?
→ upgrade Apple Silicon runner
Reproducible runbook (5 steps):
- Establish baseline: add
echo "::notice::$(date -u +%H:%M:%S)"to each step; cold-run 3 times and take the median - Add caching: DerivedData + Pods + SPM; run 10 times and track hit rate (target > 60%)
- Split the workflow: separate test and archive jobs; archive only on main
- Evaluate hardware: consider Apple Silicon only if still > 12 min after structural fixes
- Monitor p95: target ≤ 1.5× mean; alert when 20% above baseline
7. Common mistakes (iOS CI optimization)
❌ Xcode incremental build in CI works automatically
Wrong. CI wipes DerivedData unless you explicitly cache it.
❌ upgrade Mac solves CI slow
Wrong. Without caching, hardware gains are limited (~18%).
❌ increase -jobs always faster
Exceeding CPU core count slows builds. On M2, -jobs 6–8 is the sweet spot.
❌ cache key should be exact
Commit SHA causes cache misses every push. Use Podfile.lock / Package.resolved hashes instead.
8. FAQ (GitHub Actions iOS CI)
Q1: Why is GitHub Actions iOS CI so slow?
GitHub Actions macOS runners are stateless ephemeral environments — DerivedData, Pods, and SPM caches do not survive between runs. Your Mac keeps warm caches; CI cold-starts every time, even when local Xcode build times are fast.
Q2: What is the most effective iOS CI optimization?
Cache DerivedData + CocoaPods + SPM. That alone accounts for 42% of the savings (~8–10 minutes) and is the top priority for Xcode build time optimization in CI.
Q3: Can Apple Silicon fix slow CI?
Not on its own — it raises the performance ceiling. Without caching, you save ~18%. Combined with cache and parallelization, build time drops from 12 min to 9.
Q4: Which is slower — Pods or SPM?
CocoaPods is usually slower (3m 12s vs 1m 44s) because of network downloads. Cache both; the gains stack.
Q5: What is the best optimization order?
Cache → Parallelization → Apple Silicon. Together they cut 68%; do not skip steps.
9. Conclusion
Slow iOS CI is not a performance problem — it is a pipeline design problem:
Once structural optimization is done, teams that need a dedicated Apple Silicon self-hosted runner without hardware ops can explore the Cloud Mac option — zero queue wait, cache not capped at 10 GB per repo.