В типичном iOS-проекте на GitHub Actions и Xcode долгая сборка iOS CI часто считается нормой. На деле iOS CI slow ≠ Xcode slow — корень в stateless pipeline без кэша и последовательной архитектуре.
Статья основана на реальном кейсе команды из 8 iOS-разработчиков (460 000 строк Swift, 28 Pods, 14 SPM-пакетов, 30+ push в день): GitHub Actions macOS CI сократили с 28 до 9 минут — с полным планом iOS CI optimization.
1. Постановка задачи: почему iOS CI тормозит?
Многие команды мирятся с 20–30 минутами ожидания. Узкое место — не Xcode, а структура CI pipeline.
1.1 Суть проблемы GitHub Actions CI
macOS runner в GitHub Actions — это эфемерная среда (ephemeral environment). Каждый запуск означает:
- ❌ Потеря DerivedData — нет инкрементальной сборки
- ❌ CocoaPods переустанавливается каждый раз
- ❌ SPM resolve заново на каждом запуске
- ❌ Build context не переиспользуется
Итог: каждый запуск — cold build.
1.2 Разбивка baseline (28 минут)
Холодный запуск через workflow_dispatch, 3 прогона, медиана по фазам:
| Фаза | Время | Тип |
|---|---|---|
| Checkout | 45s | фикс. |
| pod install | 3m 12s | сеть |
| SPM resolve | 1m 44s | resolve |
| xcodebuild build | 11m 08s | cold compile |
| xcodebuild test | 6m 22s | CPU |
| xcodebuild archive | 5m 30s | последовательная блокировка |
| upload/sign | 1m 05s | фикс. |
| Итого | 29m 46s |
Ключевые узкие места: три причины медленной iOS CI
- Cold Build (39 %) — без DerivedData каждый раз компилируются все 460 000 строк Swift
- Dependency Re-resolve (17 %) — Pods и SPM заново скачиваются и резолвятся
- Serial Pipeline (19 %) — test и archive идут подряд, хотя зависимости между ними нет
2. Цель и результат
Результаты оптимизации (обзор iOS CI optimization)
| Этап | Время сборки |
|---|---|
| baseline | 28 min |
| + cache | 20 min |
| + parallel | 12 min |
| + Apple Silicon | 9 min |
Разбивка выигрыша
- Cache: 42 % (около 8 минут)
- Parallelization: 32 % (около 6 минут)
- Apple Silicon: 26 % (около 3 минут)
Контекст кейса (E-E-A-T)
| Параметр | Значение |
|---|---|
| Объём Swift | ~460 000 строк (с тестами) |
| CocoaPods / SPM | 28 Pods + 14 packages |
| Baseline runner | GitHub macos-latest (Intel, 4 ядра) |
| Команда / частота | 8 человек, 30+ push/день |
| Методика | таймстамп на step, 3 холодных прогона, медиана |
Однофакторные тесты (10 прогонов на конфиг, медиана): только кэш −29 %, только параллелизация −21 %, только Apple Silicon −18 %. Структурная оптимизация (кэш + параллель) даёт 74 % — железо задаёт потолок, но не является первым рычагом. p95 снизился с 33 до 12 мин, hit rate DerivedData — 80 %.
3. Рычаг 1: кэш CI (максимальный эффект)
3.1 Почему кэш критичен
Суть iOS CI optimization: вернуть инкрементальную сборку Xcode в CI. Нужно персистить DerivedData, кэш CocoaPods и кэш Swift Package Manager.
3.2 Три каталога для кэширования
- DerivedData:
~/Library/Developer/Xcode/DerivedData - CocoaPods cache:
~/Library/Caches/CocoaPods - SPM cache:
~/.spm-cache
3.3 Конфигурация 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 Дизайн cache key
Hit rate определяет, работает ли DerivedData cache по-настоящему. Три типичные стратегии:
| Стратегия key | Hit rate | Проблема |
|---|---|---|
только runner.os | ~100 % | устаревшие Pods, несогласованные артефакты |
| хэш lock-файлов (рекомендуется) | ~80 % | hit при неизменных зависимостях, rebuild только при обновлении |
| commit SHA в key | ~0 % | miss на каждом push — кэш бесполезен |
Рекомендация: хэш Podfile.lock / Package.resolved как primary key плюс restore-keys для prefix match. При miss primary key остаётся предыдущий валидный кэш — инкрементальная сборка вместо полного cold build. В CI обязательно pod install --no-repo-update, чтобы не инвалидировать кэш перезаписью lock-файлов.
Подробнее — документация GitHub Actions cache; сравнение удалённого и локального кэша — DerivedData / Pods / sccache.
3.5 Эффект кэширования
| Шаг | до | после |
|---|---|---|
| pod install | 3m 12s | 50s |
| SPM resolve | 1m 44s | 15s |
| build | 11m | 3–4m |
В среднем экономия 8–10 минут.
4. Рычаг 2: параллелизация CI (pipeline optimization)
4.1 Проблема
Стандартный CI: build → test → archive (последовательно). При этом test и archive не зависят друг от друга — их можно запускать параллельно.
4.2 Параллельные 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 только на main; каждый job восстанавливает кэш независимо. Сертификаты через Fastlane Match. Изоляция ресурсов: 3 Cloud Mac на 500 iOS CI сборок в день.
4.3 Эффект
| Режим | Время |
|---|---|
| serial | 20 min |
| parallel | 12 min |
Экономия 5–7 минут.
5. Рычаг 3: Apple Silicon runner
5.1 Почему железо — последний шаг
Железо — не первый ответ на медленную iOS CI. Без кэша Apple Silicon даёт лишь ~18 %; после структурной оптимизации время падает с 12 до 9 минут.
5.2 Замеренный прирост (кэш + параллель включены)
| Фаза | Intel | Apple Silicon |
|---|---|---|
| build | 3m 40s | 1m 55s |
| test | 6m 22s | 3m 48s |
| archive | 5m 30s | 3m 10s |
5.3 Вывод
Дополнительно 3–5 минут. Apple Silicon сильнее всего ускоряет компиляцию Swift и линковку — см. документацию Apple по инкрементальным сборкам.
5.4 Сравнение трёх вариантов macOS runner
После структурной оптимизации команды выбирают между тремя подходами GitHub Actions self-hosted runner macOS:
| Вариант | Время сборки | Очередь | Затраты на поддержку |
|---|---|---|---|
| GitHub macos-latest | ~20 min | высокая (пики: 10–20 мин ожидания) | нулевые |
| Свой Mac mini (Intel) | ~14 min | нет | высокие |
| Cloud Mac (Apple Silicon) | ~9 min | нет | низкие |
Hosted runner GitHub бесплатен, но очередь macOS runner становится узким местом; кэш ограничен 10 ГБ на repo — крупные проекты часто теряют кэш. Self-hosted убирает очередь, но Intel упирается в потолок. При 30+ push/день выделенный Apple Silicon узел обычно даёт лучший ROI: сначала оптимизируйте структуру, железо и очередь оценивайте отдельно.
6. Дерево решений 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 (5 шагов):
- Замерить baseline:
echo "::notice::$(date -u +%H:%M:%S)"на каждый step, 3 холодных прогона, медиана - Включить кэш: DerivedData + Pods + SPM, 10 прогонов, цель hit rate > 60 %
- Разделить workflow: test / archive — отдельные jobs, archive только на main
- Оценить железо: Apple Silicon runner — если после структуры всё ещё > 12 min
- Мониторить p95: цель ≤ 1,5× среднего, алерт при +20 % к baseline
7. Типичные ошибки (iOS CI Optimization Mistakes)
❌ Xcode incremental build in CI works automatically
Неверно. Без явного кэша DerivedData инкрементальной сборки в CI не будет.
❌ upgrade Mac solves CI slow
Неверно. Без кэша выигрыш ~18 %.
❌ increase -jobs always faster
Выше числа ядер CPU — медленнее. На M2 рекомендуем -jobs 6~8.
❌ cache key should be exact
Commit SHA даёт cache miss. Используйте хэш Podfile.lock / Package.resolved.
8. FAQ (частые вопросы по GitHub Actions iOS CI)
В1: Почему GitHub Actions iOS CI такой медленный?
macOS runner — stateless ephemeral environment: DerivedData, Pods и SPM-кэш теряются после каждого запуска. Локально Xcode собирает быстро с тёплым кэшем; в CI каждый раз старт с нуля.
В2: Что эффективнее всего для iOS CI optimization?
Кэш DerivedData + CocoaPods + SPM — 42 % выигрыша, 8–10 минут в среднем. Это первый приоритет для Xcode build time optimization в CI.
В3: Apple Silicon решит проблему сам по себе?
Нет — он задаёт только потолок производительности. Без кэша ~18 %; с кэшем и параллелизацией — с 12 до 9 минут.
В4: Pods или SPM — что медленнее?
Обычно CocoaPods (3m 12s vs. 1m 44s) из-за сетевых загрузок. Кэшировать нужно оба — эффекты складываются.
В5: Оптимальный порядок?
Cache → Parallelization → Apple Silicon. Вместе — 68 % сокращения; порядок не пропускать.
9. Заключение
Медленная iOS CI — не чисто performance-проблема, а:
После структурной оптимизации: если нужен выделенный Apple Silicon self-hosted runner без ops по железу — см. решение Cloud Mac (нулевая очередь, кэш без лимита 10 ГБ/repo).