Key takeaways before you tune
- xcodebuild itself is not the only culprit; cache lifecycle and machine lifecycle dominate total time.
- GitHub-hosted jobs behave like cold boots, while a self-hosted runner behaves like a warm workstation.
- DerivedData and CocoaPods cache persistence usually removes the biggest chunk of wasted CI minutes.
- Disk pressure and signing setup noise can hide as random slowdowns unless you time each stage.
- For Flutter teams, rent a mac first for a 48-hour profile, then move to a monthly self-hosted runner.
Local machine vs GitHub Actions: where the extra minutes come from
When you benchmark locally, the setup usually looks like this:
- Local: persistent SSD + warmed toolchain state; CI: ephemeral VM and cold dependency graph.
- Local: same user keychain and path conventions; CI: one-time signing bootstrap per run.
- Local: repeatable branch builds on same host; CI: many branches forcing cache misses.
A typical CI pipeline does the opposite:
- Fresh checkout on a clean workspace every run.
flutter build ipaorxcodebuild archivein Release, not Debug.- Hosted runners often cannot keep caches, so each build starts cold.
When teams say “CI is 2-3x slower”, they often compare only the final `xcodebuild archive` number. The operational truth is broader: checkout depth, pod install, simulator runtime prep, keychain unlock, and DerivedData invalidation all stack up before the compiler does real work.
Five reasons xcodebuild becomes 2-3x slower in CI
These five causes appear repeatedly in Flutter iOS pipelines, including teams on GitHub Actions and teams migrating to self-hosted runners.
- 1) Stateless runners reset every dependency layer A GitHub-hosted VM is rebuilt per job, so SPM checkout, pod install side effects, module cache, and Swift index state are never truly hot. Even with actions/cache, restore misses and key drift keep performance unstable.
- 2) DerivedData path changes invalidate build graph If DerivedData uses random temporary paths, incremental compile and index reuse collapses. xcodebuild then recompiles portions that your local machine would skip.
- 3) CocoaPods + Flutter plugin resolution does cold work Flutter plugin updates can trigger pod lock churn. Without strict lockfile discipline and deployment mode, `pod install` can spend minutes resolving and downloading.
- 4) Signing and keychain steps are serialized overhead Importing certificates, creating keychains, and unlocking signing identities in CI adds latency before the archive step. On local environments this context is already resident.
- 5) Disk and inode pressure silently throttles builds Many teams focus on CPU only. In reality, limited free SSD, cache fragmentation, and inode saturation can make indexing and file writes crawl, especially on shared hosted images.
Stage timing log (sample from a Flutter iOS workflow)
Use this table format to profile your own runs. Time each stage separately before you switch providers or rewrite workflows.
| Stage | Local Mac | GitHub Actions | What usually happened |
|---|---|---|---|
| flutter pub get | 0m25s | 1m10s | pub cache not fully warm |
| pod install | 1m30s | 4m45s | cold Specs + plugin pod resolution |
| xcodebuild compile/archive | 6m40s | 12m20s | DerivedData mismatch and no module reuse |
| codesign/export | 0m55s | 2m00s | keychain bootstrap each run |
| total | 9m30s | 20m15s | about 2.1x slower |
Before you blame xcodebuild
Before you tune xcodebuild, look at pod install. CocoaPods is slow in CI for three recurring reasons:
- Podfile.lock not committed — dependency resolution becomes unpredictable.
- No cache for
ios/Pods— hosted runners delete artifacts after the job. - Extra
pod repo update— usually unnecessary when the lockfile pins versions.
Use pod install --deployment in CI and keep Pods on the same SSD path. Self-hosted Mac mini runners avoid uploading cache archives to GitHub and downloading them again.
DerivedData, Pods, and cache: the practical architecture
For Flutter iOS CI, the biggest gains usually come from pinning DerivedData to a stable absolute path and keeping pod caches predictable. If you run a self-hosted runner, this persistence turns CI from a disposable machine into a long-lived build appliance.
If you stay on GitHub Actions hosted runners, still enforce deterministic paths and lockfiles so cache restore can do real work. If you can migrate, a self-hosted runner on rented Apple Silicon usually makes warm-path behavior close to local builds.
- Use one derivedDataPath per app target, not random temp folders.
- Run `pod install --deployment` to prevent lockfile drift.
- Watch free disk and inode consumption as first-class CI metrics.
Fix pattern: stable paths + deterministic dependency restore
The snippet below is intentionally conservative. It avoids fragile YAML tricks and focuses on path stability and predictable dependency behavior.
- Add
concurrencyto the workflow — cancel stale builds on the same branch. - Run tests on Linux — do not spend Mac minutes on
flutter testbefore xcodebuild. - Persist Pods + DerivedData — fixed paths on self-hosted hosts; granular cache keys on hosted runners.
- Build only the shipping flavor — skip scanning six flavors when you upload one IPA.
- Pin Xcode and Flutter versions — avoid
brew upgradeorflutter upgradeinside 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
This baseline is enough for many teams to cut CI duration by 30-50% before deeper optimization. For larger orgs, pair it with a dedicated self-hosted runner pool and branch-aware cache keys.
Pre-merge checklist for speed regressions
Run this list whenever iOS CI suddenly gets slower after a Flutter or Xcode update.
- ☐ Every CI run builds Release/IPA while local compares use Debug
- ☐ The job runs
flutter cleanor deletes DerivedData - ☐
pod installlogs show heavy Installing every run - ☐ GitHub-hosted runner with no local SSD cache
- ☐ 16GB machine running multiple jobs or simulators
- ☐ README-only changes still trigger a full iOS build
- ☐ Xcode version in CI does not match your local machine
If the first three boxes are checked, fix caches first. If the last two are checked, fix triggers and machine specs. See the disk guide if builds get slower over time.
FAQ
Is GitHub Actions always slower than local Mac builds?
Not always, but for Flutter iOS pipelines it is commonly slower when dependency and DerivedData layers cannot stay warm. Self-hosted runners reduce this gap.
Should I switch immediately to self-hosted runners?
Profile first. If your table shows repeated cold-start penalties, move one workflow to a self-hosted runner and compare ten-run medians before full migration.
Do I need expensive hardware to fix this?
Usually no. A rented Apple Silicon Mac with stable caches often outperforms larger but stateless hosted CI for iOS build workloads.
How do I convince the team this is worth it?
Show the stage timing table, convert saved minutes into developer waiting cost, and include release predictability benefits, not just raw speed.
Related links in this cluster
Read these in order if you are redesigning Flutter iOS CI around predictable xcodebuild performance.
- Hub: Flutter iOS CI architecture and decision map
- Cache deep dive: DerivedData, CocoaPods, and SPM strategy
- Self-hosted runner setup on cloud Mac (GitHub Actions)
- iOS code signing in CI without keychain chaos
- Disk pressure and inode monitoring for long-running runners
Need stable iOS CI speed on real Apple Silicon?
kvmboot cloud Mac gives you dedicated M-series hosts for Flutter iOS pipelines. Start with daily rent a mac for benchmarking, then keep a monthly self-hosted runner for predictable release cadence.
When your team is ready to move from debugging CI variance to shipping, view cloud Mac plans