Vuncloud 部落格
← 返回機房手記專欄

iOS CI 構建很慢怎麼辦?GitHub Actions 上 Xcode build 變慢原因解析

機房手記 · 同一條 PR 有時 6 分鐘、有時 18 分鐘?先把「快與慢」拆開看 · cold / warm · 14 天 Shadow 實測 · warm P95 14:12→6:05 ·約 9 分鐘閱讀

Mac 螢幕顯示 Xcode 與終端機,排查 GitHub Actions iOS CI xcodebuild 構建變慢
TL;DR · 先看清,再動手
  • 187 次 PR 裡,約 86.6% 是 warm——日常 SLA 該盯 warm,別把 cold 混進 P95
  • 改依賴、清快取、換 scheme 會觸發「冷啟動」,牆鐘差 2–3 倍 很常見
  • 就算全是 warm,快取 key 沒設好、多個 job 搶磁碟、測試和簽章塞在同一個 job,還是會忽快忽慢
  • 大致順序:排隊 → 分清 cold/warm → 快取 → 控並發 → 最後才談換機器(對照見 瀑布拆解

完整對照數據 → GitHub Actions 優化主軸 · Benchmark

86.6%
Shadow 樣本為 warm build
14:12→6:05
warm P95(macos-latest → 獨享 M4)
2–3×
cold vs warm 典型牆鐘差距

1. CI 像抽獎:同 commit,差好幾倍

在 GitHub Actions 上跑 iOS CI,下面這些畫面應該不陌生:

  • 同一個 commit 重跑一遍,牆鐘能差 2–3 倍
  • 儀表板 P95 紅得嚇人,團隊體感卻是「平常 merge 也沒等那麼久」
  • pod install 有時半分鐘就結束,有時像卡住了
  • 週五下午不敢點 merge——怕 CI 又「抽中」慢的那檔

未必是機器算力不夠。我們跟團隊做過 14 天 Shadow 雙軌對照,更常見的真相是:數字算混了,環境也不穩——快取留不住、磁碟被搶,體感就像抽獎。換晶片往往排在很後面。

這篇只聊一件事:構建為什麼忽快忽慢。job 卡在佇列裡等 runner?看 排隊怎麼破。快取怎麼設、買機租機划不划算,留給 快取專題ROI 那篇

2. cold 和 warm:別混在一起算

有個坑特別常見:把每次構建的耗時全倒進一個 P95。偶爾來一趟「冷啟動」,尾巴就被拉很長——看數據覺得「天天都很慢」,其實日常 merge 沒那回事。

怎麼分 · Shadow 對照用的口徑
  • warm:依賴沒動、快取還在、scheme 沒換——多半是增量編譯,代表「日常 merge 長什麼樣」
  • cold:鎖檔變了、快取清了、換 target 或 runner 上第一次跑——得重新 resolve、裝 Pod、大面積重編

日常 SLA 盯 warm 的 P50/P95 就夠了;cold 另畫一條線,別和 merge 體驗綁在一起。

2.1 什麼情況會「冷啟動」

macos-latest 上 cold 更常見——job 跑完 workspace 往往就清掉了,快取很難「住下來」:

觸發條件 典型耗時增量 日誌裡常見關鍵字
Podfile.lock 變更 +3–8 分鐘 pod install、Downloading dependencies
DerivedData 未命中 / 被清 +5–15 分鐘 CompileSwift、全量 .o 重建
切換 scheme / 新 target +2–10 分鐘 不同 xcodebuild -scheme
SPM 依賴解析變更 +1–5 分鐘 Resolve Package Graph
Xcode 小版本被動升級(macos-latest) 首次構建 +10–20 分鐘 新 SDK / 模組快取重建

依賴升得勤,cold 自然多——不是機器退化,是這週幹的活不一樣。報告時把「升 Pod 那週」和「平常開發週」分開,數字才說得通。

2.2 warm 為什麼還是不穩

就算全是 warm,牆鐘仍可能上下飄個三成左右,常見是這些:

  • 快取沒命中:key 少寫了 arm64、沒綁分支,或多個 job 搶同一個槽
  • 同一 org 裡 macOS job 扎堆,磁碟和網路互相拖
  • 這次只編 main,下次全量 unit + UI test——工作量本就不一樣
  • 改動面不同:動 Pod 原始碼和改個 SwiftUI 預覽,compile 量差很遠
開發者查看 iOS CI 構建日誌,排查 GitHub Actions xcodebuild 變慢原因

3. 時間到底耗在哪

給 workflow 各步打個時間戳,牆鐘大致能拆成五塊。下面是 warm 時比較常見的分布(專案不同會有出入):

iOS CI wall-clock five segments (warm · illustrative)
① checkout + env setup       ~0:30 – 1:30
② pod install / SPM resolve    ~0:30 – 2:00   (cold ↑↑)
③ xcodebuild compile+link      ~3:00 – 8:00   (change surface)
④ tests (simulator / unit)     ~1:00 – 6:00   (optional, often underestimated)
⑤ archive + codesign           ~1:00 – 4:00   (release pipeline)

Typical warm P50 range: 6 – 14 minutes

實用做法:用 step 耗時或 time 分別看 pod installxcodebuild buildxcodebuild test。② 經常五分鐘往上——先想 cold 和快取;③ 忽長忽短——查 DerivedData 和並發;④ 總是拖後腿——測試拆出去,或放 nightly 全量。

4. 測試和簽章:隱形拖後腿

不少「xcodebuild 好慢」的回饋,拆開看是測試或簽章被算進同一次 build

  • Simulator 冷啟動:CI 上第一次拉起可能要等多幾分鐘,沒預熱就每個 job 重來一遍
  • UI Test 比 unit 慢一個量級,和 compile 塞一個 job 裡,P95 很難看
  • 憑證、Keychain、Profile 下載——託管 runner 上常常每次 job 重新設一遍
  • Archive、打 IPA 是發版流水線的事,別和 PR 驗證的耗時攪在一起
小建議 · PR 和發版分開算

PR:build + 輕量測試,盯 warm P95。
TestFlight / 發版:單獨 workflow、單獨指標。混在一處,「平常 merge 要等多久」永遠對不上號。

5. 14 天 Shadow 實測

下面是一組雙軌對照:macos-latest 和獨享 Mac mini M4 各跑 187 次 PR,Xcode 16.2、CocoaPods 1.15.2 對齊。完整做法見 主軸 Benchmark

分類 樣本數 占比 macos-latest P95 獨享 M4 P95
warm build 162 86.6% 14:12 6:05
cold build 25 13.4% 19:40 11:20
混在一起算(容易誤判) 187 100% ~16:00+ ~7:30+

如果把 cold 和 warm 攪在一起報 P95,日常體驗會被高估大概 15–25%,容易得出「得馬上換機器」的結論。更穩的做法:merge 體驗看 warm,升依賴那幾週單獨預期 cold

獨享 M4 上 warm 仍比 cold 快一截,說明讓 DerivedData、Pods 有固定「家」很值——快取怎麼設,我們另文細說(見 快取專題)。

6. 怎麼排:從看清到快取

排隊問題排除(或已經不大)之後,可以按這個順序來,別一上來就談買機器:

  1. 給構建打標籤:cold 還是 warm(Podfile.lock 變沒變、快取中沒中)
  2. 日常 SLA 只盯 warm P95;cold 週報或單獨一條線
  3. 把 DerivedData、Pods、SPM 的快取策略定下來 — 快取最佳實踐(籌備中)
  4. macOS job 別擠太滿:self-hosted 一台 1–2 個並發就夠;託管側也盡量別多 job 共用 workspace
  5. PR 驗證和發版/signing 拆開,別讓測試污染 merge 指標
  6. 快取都到位了還不達標,再聊 M4 / M4 Pro — 見 晶片與工時
Workflow snippet · tag warm / cold (illustrative)
- name: Classify build type
  run: |
    if git diff --name-only HEAD~1 | grep -q Podfile.lock; then
      echo "BUILD_TYPE=cold" >> $GITHUB_ENV
    else
      echo "BUILD_TYPE=warm" >> $GITHUB_ENV
    fi

- name: Record wall clock
  run: |
    echo "build_type=${BUILD_TYPE}" >> metrics.csv
    echo "wall_sec=$(( $(date +%s) - START ))" >> metrics.csv

7. 常見問題(FAQ)

同 commit 重跑差 2–3 倍,正常嗎?

macos-latest 上挺常見:一回快取全中,一回 miss 疊上排隊,就像兩檔不同的 build。先看兩次 pod install 和 cache hit 日誌,別急著換晶片。

P95 到底怎麼算才對?

日常 merge 的體感,用 warm 算 P95 就行,兩週攢夠三十次樣本比較穩。cold 另算或只記次數——混在一起,採購決策容易偏。

pod install 每次都很慢,是 CocoaPods 的鍋嗎?

託管 runner 上更常是 Pods 沒地方「住」、cache key 沒綁分支和架構。self-hosted 指到固定大碟,warm 時半分鐘以內很常見。

換 Mac mini M4 能消除波動嗎?

對照裡 warm P95 能降一大截(−57%),波動也收斂(σ −40%),但分不清 cold/warm、快取沒設計好,獨享機照樣會有尖刺。

慢是排隊還是 build 本身?

看日誌開頭 Waiting for a runner 等了多久。排隊算牆鐘、不算 GitHub 分鐘費。queue 幾乎為零還慢,就是這篇聊的構建側 → 接著看快取。

還想往下讀?

快取 YAML 和 key 怎麼設計 → 快取專題;買還是租、划不划算 → ROI 模型月構建 500 次怎麼選

8. 做個總結

GitHub Actions 上 iOS CI「慢」,很少是單純 CPU 不夠。更常是這幾件事疊在一起:

  1. cold 和 warm 混在一起看,P95 被抬高了
  2. 託管 job 跑完就清碟,快取留不住
  3. 測試、簽章和 PR build 攪在一個 job 裡

先把 warm 的 P95 算清楚,再快取 → 並發 → 硬體。14 天 Shadow 裡,只做對指標和快取,warm P95 就能從 14:12 拉到 6:05——不必先拍板買機器。

想讓 DerivedData 有個固定「家」?

Vuncloud Cloud Mac M4 Pro 帶 1TB 資料碟、預裝 actions-runner,DerivedData / Pods 可以一直留在碟上,託管 runner 那種「每次冷啟動」會少很多。

查看 Cloud Mac 方案 · CI/CD 接入 FAQ

機房手記 · iOS CI

先把快慢看清,再談換晶片

cold / warm 分開算 · 快取優先 · Shadow 雙軌一週見分曉

讀主軸 Benchmark
限時優惠 點擊查看方案