In typischen iOS-Projekten mit GitHub Actions und Xcode gilt lange iOS-CI-Build-Zeit oft fälschlich als Normalzustand. Die Ursache liegt selten in Xcode selbst: iOS CI slow ≠ Xcode slow — entscheidend sind stateless Pipeline, fehlender Cache und serielle Job-Struktur.
Dieser Artikel dokumentiert einen realen Fall aus einem 8-köpfigen iOS-Team (460.000 Zeilen Swift, 28 Pods, 14 SPM-Pakete, 30+ Pushes pro Tag): GitHub Actions macOS CI von 28 auf 9 Minuten — inklusive vollständigem iOS CI optimization-Runbook.
1. Problemstellung: Warum dauert iOS CI so lange?
Viele Teams akzeptieren 20–30 Minuten Wartezeit als unvermeidlich. Der Engpass ist nicht Xcode, sondern die Architektur der CI-Pipeline.
1.1 Das Grundproblem bei GitHub Actions CI
GitHub Actions macOS Runner sind eine ephemerale Umgebung (ephemeral environment). Jeder Lauf bedeutet:
- ❌ DerivedData geht verloren — kein inkrementeller Build
- ❌ CocoaPods wird bei jedem Lauf neu installiert
- ❌ SPM resolved bei jedem Lauf neu
- ❌ Build-Kontext kann nicht wiederverwendet werden
Ergebnis: Jeder Lauf ist ein Cold Build.
1.2 Baseline-Aufschlüsselung (28 Minuten)
Mit workflow_dispatch dreimal kalt gestartet, Median pro Phase:
| Phase | Dauer | Typ |
|---|---|---|
| Checkout | 45s | fix |
| pod install | 3m 12s | Netzwerk |
| SPM resolve | 1m 44s | Auflösung |
| xcodebuild build | 11m 08s | Cold Compile |
| xcodebuild test | 6m 22s | CPU |
| xcodebuild archive | 5m 30s | seriell blockierend |
| upload/sign | 1m 05s | fix |
| Gesamt | 29m 46s |
Kern-Engpässe: drei Hauptursachen für langsame iOS CI
- Cold Build (39 %) — ohne DerivedData wird bei jedem Lauf die gesamte Swift-Basis (460.000 Zeilen) kompiliert
- Dependency Re-resolve (17 %) — Pods und SPM werden jedes Mal neu geladen und aufgelöst
- Serial Pipeline (19 %) — Test und Archive laufen nacheinander, obwohl keine Abhängigkeit besteht
2. Ziel und Ergebnis
Optimierungsergebnis (iOS CI optimization im Überblick)
| Phase | Build-Zeit |
|---|---|
| Baseline | 28 min |
| + Cache | 20 min |
| + Parallel | 12 min |
| + Apple Silicon | 9 min |
Aufschlüsselung der Einsparungen
- Cache: 42 % (ca. 8 Minuten)
- Parallelisierung: 32 % (ca. 6 Minuten)
- Apple Silicon: 26 % (ca. 3 Minuten)
Fallkontext (E-E-A-T)
| Parameter | Wert |
|---|---|
| Swift-Codebasis | ca. 460.000 Zeilen (inkl. Tests) |
| CocoaPods / SPM | 28 Pods + 14 Packages |
| Baseline-Runner | GitHub macos-latest (Intel, 4 Kerne) |
| Team / Frequenz | 8 Personen, 30+ Pushes/Tag |
| Messmethode | Zeitstempel pro Step, 3× kalt, Median |
Einzelvariable-Tests (je Konfiguration 10 Läufe, Median): nur Cache −29 %, nur Parallelisierung −21 %, nur Apple Silicon −18 %. Strukturoptimierung (Cache + Parallel) liefert 74 % — Hardware setzt das Limit, ist aber nicht der erste Hebel. p95 sank von 33 auf 12 Minuten, DerivedData-Trefferquote 80 %.
3. Maßnahme 1: CI-Cache (größter Hebel)
3.1 Warum Cache entscheidend ist
Der Kern von iOS CI optimization: Xcode-Incremental-Builds müssen in CI wieder funktionieren. Dafür müssen DerivedData, CocoaPods-Cache und Swift-Package-Manager-Cache persistent sein.
3.2 Drei Cache-Verzeichnisse
- DerivedData:
~/Library/Developer/Xcode/DerivedData - CocoaPods-Cache:
~/Library/Caches/CocoaPods - SPM-Cache:
~/.spm-cache
3.3 GitHub Actions Konfiguration
- 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
Die Trefferquote bestimmt, ob DerivedData cache wirklich greift. Drei gängige Strategien:
| Key-Strategie | Trefferquote | Problem |
|---|---|---|
nur runner.os | ~100 % | veraltete Pods möglich, inkonsistente Artefakte |
| Lockfile-Hash (empfohlen) | ~80 % | Treffer bei unveränderten Dependencies, Rebuild nur bei Update |
| Commit-SHA enthalten | ~0 % | jeder Push miss — Cache wirkungslos |
Empfehlung: Hash von Podfile.lock / Package.resolved als Primary Key plus restore-keys für Präfix-Matching. Bei Primary-Miss bleibt der letzte gültige Cache verfügbar — inkrementeller statt vollständiger Cold Build. In CI unbedingt pod install --no-repo-update, damit Lockfiles nicht versehentlich invalidiert werden.
Details in der GitHub Actions Cache-Dokumentation; Vergleich Remote- vs. lokaler Cache in DerivedData / Pods / sccache im Vergleich.
3.5 Cache-Wirkung
| Schritt | vorher | nachher |
|---|---|---|
| pod install | 3m 12s | 50s |
| SPM resolve | 1m 44s | 15s |
| build | 11m | 3–4m |
Im Schnitt 8–10 Minuten gespart.
4. Maßnahme 2: CI-Parallelisierung (Pipeline optimization)
4.1 Das Problem
Standard-CI: build → test → archive (seriell). Test und Archive hängen nicht voneinander ab — sie können parallel laufen.
4.2 Parallele Jobs
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 nur auf main; jeder Job stellt den Cache unabhängig wieder her. Zertifikate per Fastlane Match. Ressourcen-Isolation: 3 Cloud Macs für 500 iOS-CI-Läufe pro Tag.
4.3 Wirkung
| Modus | Zeit |
|---|---|
| seriell | 20 min |
| parallel | 12 min |
5–7 Minuten gespart.
5. Maßnahme 3: Apple-Silicon-Runner
5.1 Warum Hardware zuletzt kommt
Hardware ist nicht der erste Hebel gegen langsame iOS CI. Ohne Cache bringt Apple Silicon nur ~18 %; nach Strukturoptimierung sinkt die Zeit von 12 auf 9 Minuten.
5.2 Gemessene Verbesserung (Cache + Parallel aktiv)
| Phase | Intel | Apple Silicon |
|---|---|---|
| build | 3m 40s | 1m 55s |
| test | 6m 22s | 3m 48s |
| archive | 5m 30s | 3m 10s |
5.3 Fazit
Weitere 3–5 Minuten Einsparung. Apple Silicon beschleunigt vor allem Swift-Kompilierung und Linking — siehe Apple-Dokumentation zu inkrementellen Builds.
5.4 Drei macOS-Runner-Optionen im Vergleich
Nach der Strukturoptimierung wählen Teams meist zwischen drei GitHub Actions self-hosted runner macOS-Ansätzen:
| Option | Build-Zeit | Warteschlange | Wartungsaufwand |
|---|---|---|---|
| GitHub macos-latest | ~20 min | hoch (Spitzen 10–20 min Wartezeit) | keiner |
| Eigener Mac mini (Intel) | ~14 min | keine | hoch |
| Cloud Mac (Apple Silicon) | ~9 min | keine | gering |
GitHub-Hosted-Runner sind kostenlos, aber die macOS-Runner-Warteschlange wird zum Flaschenhals; Cache ist auf 10 GB/Repo begrenzt — große Projekte erleben häufige Evictions. Eigene Hardware eliminiert die Queue, Intel setzt aber ein hartes Limit. Bei 30+ Pushes/Tag lohnt sich ein dedizierter Apple-Silicon-Knoten meist am ehesten: Struktur selbst optimieren, Hardware und Queue separat bewerten.
6. Entscheidungsbaum für iOS CI optimization
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
Reproduzierbares Runbook (5 Schritte):
- Baseline messen: pro Step
echo "::notice::$(date -u +%H:%M:%S)", 3× kalt, Median - Cache aktivieren: DerivedData + Pods + SPM, 10 Läufe, Trefferquote > 60 % anstreben
- Workflow splitten: test / archive als eigene Jobs, Archive nur auf main
- Hardware prüfen: Apple-Silicon-Runner erst, wenn Strukturoptimierung > 12 min hinterlässt
- p95 überwachen: Ziel ≤ 1,5× Mittelwert, Alarm bei +20 % über Baseline
7. Typische Fehler (iOS CI Optimization Mistakes)
❌ Xcode incremental build in CI works automatically
Falsch. Ohne DerivedData-Cache gibt es in CI keinen inkrementellen Build.
❌ upgrade Mac solves CI slow
Falsch. Ohne Cache nur ~18 % Gewinn.
❌ increase -jobs always faster
Über der CPU-Kernzahl wird es langsamer. Auf M2 empfehlen wir -jobs 6~8.
❌ cache key should be exact
Commit-SHA erzwingt Cache-Miss. Hash aus Podfile.lock / Package.resolved verwenden.
8. FAQ (häufige Fragen zu GitHub Actions iOS CI)
F1: Warum ist GitHub Actions iOS CI so langsam?
Weil macOS Runner stateless und ephemeral sind — DerivedData, Pods und SPM-Cache gehen nach jedem Lauf verloren. Lokal baut Xcode mit Warm-Cache schnell; in CI startet jeder Lauf bei null.
F2: Was bringt bei iOS CI optimization am meisten?
Cache für DerivedData + CocoaPods + SPM — 42 % Beitrag, 8–10 Minuten im Schnitt. Das ist die erste Priorität für Xcode build time optimization in CI.
F3: Löst Apple Silicon das CI-Problem allein?
Nein — es setzt nur das Performance-Limit. Ohne Cache ~18 %; mit Cache und Parallelisierung von 12 auf 9 Minuten.
F4: Pods oder SPM — was ist langsamer?
CocoaPods meist langsamer (3m 12s vs. 1m 44s) wegen Netzwerk-Downloads. Beide cachen — Effekte addieren sich.
F5: Optimale Reihenfolge?
Cache → Parallelisierung → Apple Silicon. Kombiniert 68 % Einsparung; Reihenfolge nicht überspringen.
9. Fazit
Langsame iOS CI ist kein reines Performance-Problem, sondern:
Nach der Strukturoptimierung: Wer einen dedizierten Apple-Silicon-Self-Hosted-Runner ohne Hardware-Betrieb sucht, findet in der Cloud-Mac-Lösung null Wartezeit in der Queue und Cache ohne 10-GB/Repo-Limit.