Акция

Почему xcodebuild в CI в 2–3 раза медленнее, чем локально?

CI Flutter iOS CI
2026-06-08 ~12 мин

Материал для Flutter iOS команд: как измерить реальную структуру задержек в CI и выбрать путь к стабильной скорости.

Flutter iOS CI: разница времени локально и в CI
Локально сборка может выглядеть нормальной, но в GitHub Actions конвейер часто стартует с нуля, и суммарное время вокруг xcodebuild вырастает в 2-3 раза.

Что важно понять сразу

  1. Проблема не только в xcodebuild: решает жизненный цикл кешей и самой машины.
  2. GitHub-hosted почти всегда cold start; self-hosted runner работает как прогретая рабочая станция.
  3. Персистентные DerivedData и CocoaPods обычно дают самый большой выигрыш по времени.
  4. Диск и шаги подписи часто маскируются под «медленный компилятор».
  5. Для Flutter iOS рационально начать с аренда Mac на 48 часов, затем перейти на месячный self-hosted runner.

Локальный Mac против GitHub Actions: откуда берутся лишние минуты

Локально команды обычно сравнивают в таких условиях:

  • Локально: постоянный SSD и теплый toolchain; CI: эфемерная VM и холодные зависимости.
  • Локально: уже готовый signing контекст; CI: повторный keychain bootstrap.
  • Локально: повторные сборки на одном хосте; CI: больше cache miss из-за веток.

В CI-пайплайне чаще всего наоборот:

  • Каждый запуск — чистый workspace после checkout.
  • flutter build ipa или xcodebuild archive в Release.
  • На hosted runner кеши не держатся — фактически cold start.

Обычно сравнивают лишь `xcodebuild archive`, но реальные потери накапливаются раньше: checkout, pod install, bootstrap keychain, инвалидирование DerivedData. В сумме это и дает эффект 2-3x.

Пять причин, почему xcodebuild в CI медленнее в 2-3 раза

Эти причины регулярно встречаются в Flutter iOS пайплайнах на GitHub Actions и при миграции на self-hosted runner.

  1. 1) Stateless runner сбрасывает все dependency-слои GitHub-hosted VM создается заново для каждого job, поэтому SPM, Pods и модульные кеши не успевают стабильно прогреваться.
  2. 2) Плавающий путь DerivedData ломает инкрементальность Если путь меняется, xcodebuild хуже переиспользует результаты и компилирует лишнее.
  3. 3) Flutter plugins провоцируют холодное pod-разрешение При дрейфе lockfile `pod install` тратит минуты на разрешение и скачивание. Нужна жесткая воспроизводимость.
  4. 4) Подпись и keychain дают последовательный overhead Импорт сертификатов, создание и unlock keychain в CI происходят почти в каждом запуске и добавляют фиксированную задержку.
  5. 5) Давление на диск и inode замедляет запись Одного мониторинга CPU недостаточно. Недостаток свободного места и inode замедляет индексирование и вывод артефактов.

Таблица таймингов этапов (пример Flutter iOS)

Сначала соберите такую таблицу по 10+ прогонам, и только потом меняйте платформу или архитектуру.

Этап Локальный Mac GitHub Actions Типичная причина
flutter pub get0m25s1m10spub cache не прогрет
pod install1m30s4m45sхолодные Specs и plugin pods
xcodebuild compile/archive6m40s12m20sDerivedData не переиспользуется
codesign/export0m55s2m00sbootstrap keychain каждый запуск
итого9m30s20m15sпримерно в 2.1 раза медленнее

Прежде чем винить xcodebuild

Перед оптимизацией xcodebuild проверьте pod install. CocoaPods в CI обычно тормозит по трём причинам:

  • Не закоммичен Podfile.lock — нестабильное разрешение зависимостей.
  • Нет кеша ios/Pods — артефакты удаляются после job.
  • Лишний pod repo update — при lockfile обычно не нужен.

В CI используйте pod install --deployment и храните Pods на одном SSD-пути. Self-hosted Mac mini избавляет от загрузки кешей в GitHub.

DerivedData, Pods и кеши: практичная архитектура

Для Flutter iOS наибольший эффект обычно дает фиксированный абсолютный путь DerivedData и предсказуемый CocoaPods cache. На self-hosted runner CI становится не одноразовой VM, а управляемым сборочным контуром.

Даже на GitHub-hosted помогает детерминированность путей и lockfile. Если миграция возможна, аренда Mac на Apple Silicon и переход на self-hosted обычно приближают поведение к локальному.

  • Фиксируйте derivedDataPath для каждого target.
  • Используйте `pod install --deployment` против дрейфа lockfile.
  • Мониторьте свободный диск и inode как ключевые CI-метрики.

Паттерн исправления: стабильные пути + детерминированные зависимости

Ниже базовый, эксплуатационно устойчивый вариант без хрупких YAML-трюков.

  1. Добавить concurrency — отменять устаревшие сборки ветки.
  2. Тесты на Linux — не тратить Mac-время на flutter test.
  3. Персистентные Pods + DerivedData — фиксированные пути self-hosted, тонкие cache keys hosted.
  4. Собирать только shipping flavor — не сканировать шесть flavor ради одного IPA.
  5. Зафиксировать версии Xcode/Flutter — без upgrade в CI.
# .github/workflows/ios-ci.yml
jobs:
  ios:
    runs-on: [self-hosted, macOS, ARM64, flutter-ios]
    steps:
      - uses: actions/checkout@v4
      - name: Restore caches
        run: |
          mkdir -p "$HOME/Library/Caches/CocoaPods"
          mkdir -p "$HOME/.pub-cache"
      - name: Build with stable DerivedData path
        run: |
          flutter pub get
          cd ios
          xcodebuild             -workspace Runner.xcworkspace             -scheme Runner             -configuration Release             -destination 'generic/platform=iOS'             -derivedDataPath "$HOME/ci-derived/runner"             CODE_SIGNING_ALLOWED=NO
      - name: Keep pod install deterministic
        run: |
          cd ios
          pod install --deployment

Этого часто хватает, чтобы вернуть 30-50% времени. Далее масштабируйте через пул self-hosted runner.

Чеклист перед merge против регрессий скорости

Запускайте после обновлений Flutter/Xcode и при любом скачке времени.

  • ☐ CI собирает Release/IPA, локально сравнивают Debug
  • ☐ В job есть flutter clean или удаление DerivedData
  • ☐ В логах pod install каждый раз много Installing
  • ☐ GitHub-hosted runner без локального SSD-кеша
  • ☐ 16 ГБ машина с несколькими job или симуляторами
  • ☐ Изменение README запускает полную iOS-сборку
  • ☐ Версия Xcode в CI не совпадает с локальной

Первые три пункта — чинить кеши. Последние два — триггеры и характеристики машины.

FAQ

GitHub Actions всегда медленнее локальной сборки?

Не всегда, но для Flutter iOS это частый сценарий при холодных кеш-слоях. Self-hosted runner обычно уменьшает разрыв.

Нужно сразу мигрировать на self-hosted runner?

Сначала измерения. Если доминируют cold start потери, перенесите один workflow и сравните медиану по 10 прогонам.

Нужен дорогой сервер для ускорения?

Обычно нет. аренда Mac на Apple Silicon с устойчивыми кешами дает заметный эффект.

Как обосновать это команде?

Покажите таблицу этапов, переведите минуты в стоимость ожидания и добавьте выигрыш в предсказуемости релизов.

Если вы перестраиваете Flutter iOS CI под предсказуемую скорость, двигайтесь по этому порядку.

Нужна стабильная скорость iOS CI на реальном Apple Silicon?

kvmboot cloud Mac дает выделенные M-series хосты для Flutter iOS. Сначала аренда Mac посуточно для профилирования, затем месячный self-hosted runner.

Если хотите заменить CI-лотерею предсказуемыми релизами, посмотреть тарифы cloud Mac