Dans un projet iOS classique avec GitHub Actions et Xcode, une durée de build CI iOS élevée est souvent prise pour une fatalité. En réalité : iOS CI slow ≠ Xcode slow — la cause est une pipeline sans état, sans cache et conçue en série.
Cet article s'appuie sur un cas réel (équipe iOS de 8 personnes, 460 000 lignes Swift, 28 Pods, 14 packages SPM, 30+ push/jour) : passage de GitHub Actions macOS CI de 28 à 9 minutes, avec un plan complet d'iOS CI optimization.
1. Cadrage : pourquoi la CI iOS est-elle lente ?
Beaucoup d'équipes acceptent 20–30 minutes d'attente. Le goulot n'est pas Xcode, mais l'architecture de la pipeline CI.
1.1 Le problème structurel de GitHub Actions CI
Les runners macOS GitHub Actions sont un environnement éphémère (ephemeral environment). Chaque exéction implique :
- ❌ Perte de DerivedData — pas de build incrémental
- ❌ CocoaPods réinstallé à chaque run
- ❌ SPM re-résolu à chaque run
- ❌ Impossible de réutiliser le contexte de build
Résultat : chaque run est un cold build.
1.2 Décomposition de la baseline (28 minutes)
Lancement à froid via workflow_dispatch, 3 runs, médiane par phase :
| Phase | Durée | Type |
|---|---|---|
| Checkout | 45s | fixe |
| pod install | 3m 12s | réseau |
| SPM resolve | 1m 44s | résolution |
| xcodebuild build | 11m 08s | compilation à froid |
| xcodebuild test | 6m 22s | CPU |
| xcodebuild archive | 5m 30s | séquentiel bloquant |
| upload/sign | 1m 05s | fixe |
| Total | 29m 46s |
Goulots clés : trois causes d'une CI iOS lente
- Cold Build (39 %) — sans DerivedData, 460 000 lignes Swift recompilées à chaque run
- Dependency Re-resolve (17 %) — Pods et SPM retéléchargés et re-résolus à chaque fois
- Serial Pipeline (19 %) — test et archive enchaînés sans dépendance réelle
2. Objectif et résultats
Résultats d'optimisation (vue d'ensemble iOS CI optimization)
| Étape | Temps de build |
|---|---|
| baseline | 28 min |
| + cache | 20 min |
| + parallèle | 12 min |
| + Apple Silicon | 9 min |
Répartition des gains
- Cache : 42 % (environ 8 minutes)
- Parallélisation : 32 % (environ 6 minutes)
- Apple Silicon : 26 % (environ 3 minutes)
Contexte du cas (E-E-A-T)
| Paramètre | Valeur |
|---|---|
| Code Swift | ~460 000 lignes (tests inclus) |
| CocoaPods / SPM | 28 Pods + 14 packages |
| Runner baseline | GitHub macos-latest (Intel, 4 cœurs) |
| Équipe / fréquence | 8 personnes, 30+ push/jour |
| Méthode | horodatage par step, 3 runs à froid, médiane |
Tests à variable unique (10 runs par config, médiane) : cache seul −29 %, parallélisation seule −21 %, Apple Silicon seul −18 %. L'optimisation structurelle (cache + parallèle) apporte 74 % — le matériel fixe le plafond, ce n'est pas le premier levier. p95 passé de 33 à 12 min, taux de hit DerivedData à 80 %.
3. Levier 1 : cache CI (gain maximal)
3.1 Pourquoi le cache est central
Le cœur de l'iOS CI optimization : réactiver le build incrémental Xcode en CI. Il faut persister DerivedData, le cache CocoaPods et le cache Swift Package Manager.
3.2 Trois répertoires à cacher
- DerivedData :
~/Library/Developer/Xcode/DerivedData - Cache CocoaPods :
~/Library/Caches/CocoaPods - Cache SPM :
~/.spm-cache
3.3 Configuration GitHub Actions
- 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 Conception des clés de cache
Le taux de hit détermine si le DerivedData cache est réellement efficace. Trois stratégies courantes :
| Stratégie de clé | Taux de hit | Problème |
|---|---|---|
runner.os seul | ~100 % | Pods obsolètes possibles, artefacts incohérents |
| hash des lockfiles (recommandé) | ~80 % | hit si dépendances inchangées, rebuild uniquement à la mise à jour |
| SHA de commit inclus | ~0 % | miss à chaque push — cache inutile |
Recommandation : hash de Podfile.lock / Package.resolved comme clé principale, plus restore-keys pour le préfixe. En cas de miss principal, le dernier cache valide reste disponible — build incrémental plutôt que cold build total. En CI, utiliser pod install --no-repo-update pour ne pas invalider le cache en réécrivant les lockfiles.
Voir la documentation GitHub Actions cache ; comparaison cache distant vs. disque local dans DerivedData / Pods / sccache comparés.
3.5 Impact du cache
| Étape | avant | après |
|---|---|---|
| pod install | 3m 12s | 50s |
| SPM resolve | 1m 44s | 15s |
| build | 11m | 3–4m |
8 à 10 minutes gagnées en moyenne.
4. Levier 2 : parallélisation CI (pipeline optimization)
4.1 Le problème
CI par défaut : build → test → archive (séquentiel). Or test et archive n'ont pas de dépendance — ils peuvent tourner en parallèle.
4.2 Jobs parallèles
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 uniquement sur main ; chaque job restaure son cache indépendamment. Certificats via Fastlane Match. Isolation des ressources : 3 Cloud Mac pour 500 builds iOS CI par jour.
4.3 Impact
| Mode | Temps |
|---|---|
| séquentiel | 20 min |
| parallèle | 12 min |
5 à 7 minutes gagnées.
5. Levier 3 : runner Apple Silicon
5.1 Pourquoi le matériel vient en dernier
Le matériel n'est pas le premier levier contre une CI iOS lente. Sans cache, Apple Silicon n'apporte qu'environ 18 % ; après optimisation structurelle, on passe de 12 à 9 minutes.
5.2 Gains mesurés (cache + parallèle actifs)
| Phase | Intel | Apple Silicon |
|---|---|---|
| build | 3m 40s | 1m 55s |
| test | 6m 22s | 3m 48s |
| archive | 5m 30s | 3m 10s |
5.3 Conclusion
3 à 5 minutes supplémentaires. Apple Silicon accélère surtout la compilation Swift et le linking — voir la documentation Apple sur les builds incrémentaux.
5.4 Comparaison de trois options de runner macOS
Après optimisation structurelle, les équipes choisissent généralement entre trois approches GitHub Actions self-hosted runner macOS :
| Option | Temps de build | File d'attente | Coût de maintenance |
|---|---|---|---|
| GitHub macos-latest | ~20 min | élevée (pics : 10–20 min d'attente) | nul |
| Mac mini dédié (Intel) | ~14 min | aucune | élevé |
| Cloud Mac (Apple Silicon) | ~9 min | aucune | faible |
Les runners hébergés GitHub sont gratuits, mais la file d'attente des runners macOS devient un goulot ; le cache est limité à 10 Go/repo — les gros projets subissent des évictions fréquentes. Le self-hosted supprime la file, mais l'Intel plafonne. À 30+ push/jour, un nœud Apple Silicon dédié offre souvent le meilleur ROI : optimisez la structure d'abord, évaluez matériel et file ensuite.
6. Arbre de décision 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
Runbook reproductible (5 étapes) :
- Établir la baseline :
echo "::notice::$(date -u +%H:%M:%S)"par step, 3 runs à froid, médiane - Activer le cache : DerivedData + Pods + SPM, 10 runs, viser un taux de hit > 60 %
- Découper le workflow : jobs test / archive séparés, archive uniquement sur main
- Évaluer le matériel : runner Apple Silicon seulement si la structure laisse > 12 min
- Surveiller le p95 : objectif ≤ 1,5× la moyenne, alerte à +20 % au-dessus de la baseline
7. Erreurs fréquentes (iOS CI Optimization Mistakes)
❌ Xcode incremental build in CI works automatically
Faux. Sans cache DerivedData explicite, pas de build incrémental en CI.
❌ upgrade Mac solves CI slow
Faux. Sans cache, gain limité (~18 %).
❌ increase -jobs always faster
Au-delà du nombre de cœurs CPU, ça ralentit. Sur M2, -jobs 6~8 est recommandé.
❌ cache key should be exact
Le SHA de commit force un cache miss. Utiliser le hash de Podfile.lock / Package.resolved.
8. FAQ (questions fréquentes GitHub Actions iOS CI)
Q1 : Pourquoi GitHub Actions iOS CI est-il si lent ?
Les runners macOS sont stateless et éphémères : DerivedData, Pods et cache SPM disparaissent à chaque run. En local, Xcode compile vite avec un cache chaud ; en CI, chaque run repart de zéro.
Q2 : Quelle est la méthode la plus efficace pour l'iOS CI optimization ?
Cache DerivedData + CocoaPods + SPM — 42 % du gain, 8–10 minutes en moyenne. C'est la première priorité pour l'Xcode build time optimization en CI.
Q3 : Apple Silicon résout-il seul la lenteur CI ?
Non — il fixe seulement le plafond de performance. Sans cache ~18 % ; avec cache et parallélisation, de 12 à 9 minutes.
Q4 : Pods ou SPM — lequel est plus lent ?
CocoaPods en général (3m 12s vs. 1m 44s) à cause des téléchargements réseau. Les deux doivent être cachés — les gains s'additionnent.
Q5 : Ordre d'optimisation optimal ?
Cache → Parallélisation → Apple Silicon. Combinaison : 68 % de réduction ; ne pas sauter d'étape.
9. Conclusion
Une CI iOS lente n'est pas un problème de performance pur, mais :
Après l'optimisation structurelle, pour un runner self-hosted Apple Silicon dédié sans ops matériel : voir la solution Cloud Mac (zéro attente en file, cache sans limite 10 Go/repo).