GitHub Actions + Xcode で回している典型的な iOS プロジェクトでは、iOS CI のビルドが遅いのは「仕方ない」と諦めがちです。しかし本質は iOS CI slow ≠ Xcode slow で、原因は パイプラインのステートレス設計・キャッシュ不在・直列実行 にあります。
本記事では、8 人の iOS チーム(Swift 約 46 万行、Pods 28 個、SPM 14 個、1 日 30 回以上の push)の実例をもとに、GitHub Actions macOS CI を 28 分から 9 分へ短縮した iOS CI optimization の手順をまとめます。
一、問題の定義:なぜ iOS CI は遅いのか
多くのチームが 20〜30 分の待ち時間を「当たり前」と受け入れています。ボトルネックは Xcode ではなく、CI パイプラインの設計そのものです。
1.1 GitHub Actions CI の構造的な問題
GitHub Actions の macOS runner は ephemeral environment(使い捨て環境) です。ジョブが終わるたびに次が起きます:
- ❌ DerivedData が消える(incremental build 不可)
- ❌ CocoaPods を毎回 install し直す
- ❌ SPM を毎回 resolve し直す
- ❌ ビルドコンテキストを再利用できない
結果として、毎回 cold build になります。
1.2 ベースラインの時間内訳(28 分)
workflow_dispatch でクリーンビルドを 3 回実行し、中央値で各フェーズを計測しました:
| フェーズ | 所要時間 | 種別 |
|---|---|---|
| Checkout | 45s | 固定 |
| pod install | 3m 12s | ネットワーク依存 |
| SPM resolve | 1m 44s | 解決処理 |
| xcodebuild build | 11m 08s | cold compile |
| xcodebuild test | 6m 22s | CPU |
| xcodebuild archive | 5m 30s | 直列ブロック |
| upload/sign | 1m 05s | 固定 |
| Total | 29m 46s |
核心ボトルネック:iOS CI が遅い 3 大原因
- Cold Build(39%)——DerivedData なしで 46 万行 Swift を毎回フルコンパイル
- Dependency Re-resolve(17%)——Pods + SPM を毎回ダウンロード・解決
- Serial Pipeline(19%)——test と archive に依存関係がないのに直列実行
二、最適化の目標と結果
最適化結果(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 コード量 | 約 46 万行(テスト含む) |
| CocoaPods / SPM | 28 Pods + 14 packages |
| ベースライン Runner | GitHub macos-latest(Intel、4 コア) |
| チーム / 頻度 | 8 人、1 日 30 回以上の push |
| 計測方法 | 各ステップにタイムスタンプ、冷起動 3 回の中央値 |
単一変数実験(各構成を 10 回実行し中央値を取得):キャッシュのみ -29%、並列化のみ -21%、Apple Silicon のみ -18%。構造最適化(キャッシュ + 並列化)が 74% を占め、ハードウェアは上限を引き上げる要素であり第一要因ではありません。 p95 は 33 分から 12 分へ、DerivedData キャッシュヒット率は 80%。
三、最適化①:CI キャッシュ(最大の効果)
3.1 キャッシュが鍵である理由
iOS CI optimization の本質は、CI 上で Xcode の incremental build を再び有効にすることです。 DerivedData、CocoaPods cache、Swift Package Manager cache の 3 ディレクトリを永続化する必要があります。
3.2 キャッシュの 3 要素
- 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 キャッシュ key の設計ポイント
キャッシュヒット率が DerivedData cache の実効性を左右します。key 設計には 3 つの典型的なパターンがあります:
| key 戦略 | ヒット率 | 問題点 |
|---|---|---|
runner.os のみ | ~100% | 古い Pods を参照し、成果物が不整合になる恐れ |
| ロックファイルのハッシュ(推奨) | ~80% | 依存が変わらなければヒット、更新時のみ再構築 |
| commit SHA を含める | ~0% | push のたびに miss、実質キャッシュなし |
Podfile.lock / Package.resolved のハッシュを主 key にし、restore-keys でプレフィックスマッチを設定するのが定石です。主 key が miss しても前回の有効キャッシュを取得でき、完全な cold build を避けられます。CI では pod install --no-repo-update を使い、lock ファイルの書き換えによるキャッシュ無効化を防ぎましょう。
詳細は GitHub Actions cache ドキュメント を参照。macOS runner のキュー待ちについては GitHub Actions macOS runner がいつも並ぶ理由 も合わせて確認してください。
3.5 キャッシュの効果
| 項目 | 最適化前 | 最適化後 |
|---|---|---|
| pod install | 3m 12s | 50s |
| SPM resolve | 1m 44s | 15s |
| build | 11m | 3–4m |
平均 8〜10 分の短縮。
四、最適化②:CI 並列化(Pipeline optimization)
4.1 問題点
デフォルトの CI は build → test → archive と直列です。しかし test と archive には依存関係がなく、並列実行が可能です。
4.2 並列 Job の設計
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 を推奨。高頻度 CI のリソース分離については Cloud Mac 3 台で 1 日 500 回の iOS CI を回す方法 も参考になります。
4.3 効果
| モード | 時間 |
|---|---|
| serial | 20 min |
| parallel | 12 min |
5〜7 分の短縮。
五、最適化③:Apple Silicon Runner
5.1 なぜ最後にやるのか
ハードウェアは iOS CI slow を解決する第一手段ではありません。 キャッシュなしで 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 の incremental build ドキュメント も参照してください。
5.4 3 種類の macOS Runner 比較
構造最適化が終わった段階で、チームは次の 3 つの GitHub Actions self-hosted runner macOS 案から選ぶことが多いです:
| 方式 | ビルド時間 | キュー | 運用コスト |
|---|---|---|---|
| GitHub macos-latest | ~20 min | 高(ピーク時 10〜20 分待ち) | ゼロ |
| 自前 Mac mini(Intel) | ~14 min | なし | 高 |
| Cloud Mac(Apple Silicon) | ~9 min | なし | 低 |
GitHub ホスト runner は無料枠がある一方、キューがボトルネックになりやすく、キャッシュは repo あたり 10 GB 制限で大規模プロジェクトは evict が頻発します。自前 runner はキューを消せますが Intel には天井があります。1 日 30 回以上 push するチームなら、専有 Apple Silicon ノードの ROI が高いケースが多い——構造最適化は自前で、ハードとキューは別途評価するのが現実的です。
六、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 ステップ):
- ベースライン計測:各 step に
echo "::notice::$(date -u +%H:%M:%S)"を入れ、冷起動 3 回の中央値を取る - キャッシュ導入:DerivedData + Pods + SPM を設定し、10 回連続でヒット率を計測(目標 > 60%)
- workflow 分割:test / archive を独立 job に、archive は main のみ
- ハード評価:構造最適化後も > 12 min なら Apple Silicon runner を検討
- p95 監視:目標は平均の 1.5 倍以下、ベースライン比 +20% でアラート
七、よくある誤解(iOS CI Optimization Mistakes)
❌ Xcode incremental build in CI works automatically
誤り。CI には DerivedData が残らないため、明示的なキャッシュが必須です。
❌ upgrade Mac solves CI slow
誤り。キャッシュなしでは改善は限定的(約 18%)。
❌ increase -jobs always faster
CPU コア数を超えると逆に遅くなります。M2 では -jobs 6〜8 が目安。
❌ cache key should be exact
commit SHA だと毎回 miss。Podfile.lock / Package.resolved のハッシュを使いましょう。
八、FAQ(GitHub Actions iOS CI よくある質問)
Q1:GitHub Actions の iOS CI が遅いのはなぜ?
GitHub Actions の macOS runner は stateless な ephemeral environment で、DerivedData・Pods・SPM キャッシュを保持しません。ローカルではホットキャッシュがあるのに、CI は毎回ゼロから cold build するため、手元の Xcode build time が速くても CI だけ遅くなります。
Q2:iOS CI optimization で最も効く方法は?
DerivedData + CocoaPods + SPM のキャッシュで、全体の 42%(平均 8〜10 分)を短縮できます。CI 環境における Xcode build time optimization の最優先事項です。
Q3:Apple Silicon だけで CI slow は解決する?
単独では不十分で、あくまで加速の上限です。キャッシュなしでは 18% 程度。キャッシュと並列化と組み合わせて初めて 12 分から 9 分に落とせます。
Q4:Pods と SPM、どちらが遅い?
通常 CocoaPods の方が遅い(3m 12s vs 1m 44s)。ネットワークダウンロードが絡むためです。両方キャッシュすれば効果は加算されます。
Q5:最適化の推奨順序は?
Cache → Parallelization → Apple Silicon。3 段階で 68% 短縮。順番を飛ばすと効果が半減します。
九、まとめ
iOS CI が遅い本質はパフォーマンス問題ではなく:
補足:構造最適化後、専有 Apple Silicon self-hosted runner が必要でハード運用を避けたい場合は、Cloud Mac プラン(キュー待ちゼロ、キャッシュ 10 GB/repo 制限なし)をご検討ください。