先看五個重點
- 慢的不只 xcodebuild,本質是快取生命週期與機器生命週期不一致。
- GitHub Actions 託管機像每次重灌;自託管 Runner 更像長期維運的工作站。
- DerivedData 與 CocoaPods 一旦可持久化,通常就能拿回最大一段時間。
- 磁碟與簽章流程常偽裝成「編譯慢」,必須拆階段量測。
- Flutter 團隊可先用租用 Mac 做 48 小時壓測,再決定月租自託管 Runner。
本機 Mac vs GitHub Actions:多出來的時間去哪了?
很多人抱怨 CI 慢,其實對比常常不公平。本機開發多半是這樣:
- 本機:持久 SSD + 熱快取;CI:短生命週期 VM + 冷依賴。
- 本機:同一使用者簽章環境;CI:每次重建 keychain。
- 本機:同機反覆建置;CI:分支切換導致快取命中率下降。
CI 流水線的典型做法則是:
- 每次 checkout 一份乾淨的工作區;
- 跑
flutter build ipa或xcodebuild archive,走 Release; - 託管 Runner 快取往往存不住,等於每次從零編。
很多團隊只拿 `xcodebuild archive` 做比較,忽略 checkout、pod install、keychain 建置、DerivedData 失效等隱性成本。這些成本疊在一起,就會形成你感受到的 2-3 倍差距。
xcodebuild 在 CI 變慢的五個原因
以下模式在 Flutter iOS 專案很常見,無論你現在用 GitHub Actions 或準備轉自託管 Runner 都適用。
- 1) 無狀態 Runner 每次重置依賴層 GitHub 託管機每次 job 都是新環境,SPM、Pods、模組快取很難真正熱起來。即使有 actions/cache,也常因 key 漂移導致命中不穩。
- 2) DerivedData 路徑不固定讓增量編譯失效 若每次都用暫存路徑,xcodebuild 會把可重用的編譯成果視為無效,重編譯比你想像多。
- 3) Flutter 外掛連動 Pod 解析造成冷啟成本 外掛版本波動會觸發 pod 解析與下載;若沒鎖定 lockfile 與 deployment 模式,`pod install` 很容易多花數分鐘。
- 4) 簽章與 keychain 流程是固定額外開銷 憑證匯入、keychain 建立、解鎖流程在 CI 需要每次重做,本機通常是常駐狀態。
- 5) 磁碟與 inode 壓力會讓寫入吞吐變慢 只看 CPU 會漏掉真因。SSD 可用空間、快取碎片、inode 壓力都會拖慢 index 與輸出。
階段耗時表(Flutter iOS 範例)
先用這種表格量測 10 次以上,再決定要不要換平台或重寫 workflow。
| 階段 | 本機 Mac | GitHub Actions | 常見原因 |
|---|---|---|---|
| flutter pub get | 0m25s | 1m10s | pub 快取不夠熱 |
| pod install | 1m30s | 4m45s | Specs 與 plugin pod 冷解析 |
| xcodebuild compile/archive | 6m40s | 12m20s | DerivedData 無法重用 |
| codesign/export | 0m55s | 2m00s | 每次重建 keychain |
| 總計 | 9m30s | 20m15s | 約慢 2.1 倍 |
先別急著怪 xcodebuild
不少團隊盯著 xcodebuild,卻忽略前面的 pod install。CocoaPods 在 CI 裡慢,通常就三個原因:
- 沒提交
Podfile.lock——每次重新算依賴,時間不可控。 - 沒快取
ios/Pods目錄——託管 Runner 跑完就刪,下次又從 CDN 拉。 - 多跑了
pod repo update——CI 裡一般不需要,鎖檔已釘死版本。
改法很具體:CI 用 pod install --deployment,並把 Pods 目錄快取在同一台機器的 SSD 上。自託管 Mac mini 比 GitHub 託管 Runner 省事——快取直接落本機,不必先打包上傳再下載。
DerivedData、Pods 與快取的實戰架構
對 Flutter iOS CI 來說,最有效的改動通常是固定 DerivedData 絕對路徑,並讓 Pods 快取可預測。若使用自託管 Runner,CI 就從一次性機器變成可持續優化的建置節點。
即使暫時留在 GitHub 託管機,也要把路徑與 lockfile 做到可重現,讓快取還原真的發揮作用。若能遷移,自託管 Runner 在 Apple Silicon 上通常可把熱路徑表現拉近本機。
- 每個 target 使用固定 derivedDataPath,避免臨時目錄。
- 使用 `pod install --deployment` 控制 lockfile 漂移。
- 把磁碟可用量與 inode 當成 CI 核心指標。
修復模式:固定路徑 + 可預測依賴還原
下面範例刻意保持簡單,重點放在可維護性,而非華麗 YAML 技巧。
- workflow 加
concurrency——同一分支新 push 取消舊構建,少排隊。 - 測試與分析放 Linux——
flutter test別佔 Mac 時間。 - 持久化 Pods + DerivedData——自託管固定路徑;託管用
actions/cache並縮小 key 粒度。 - CI 只編要發的 flavor——別在流水線裡掃 6 個 flavor 卻只上傳 1 個包。
- 固定 Xcode / Flutter 版本——別在 CI 裡
brew upgrade或flutter upgrade。
# .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% 時間。規模更大時,再搭配分池自託管 Runner 與分支感知快取 key。
合併前的效能檢查清單
Flutter 或 Xcode 升版後,只要 CI 變慢就跑一次。
- ☐ 每次 CI 都跑 Release / IPA,本機對比的是 Debug
- ☐ Job 裡寫過
flutter clean或刪 DerivedData - ☐
pod install日誌裡每次大量 Installing - ☐ 使用 GitHub 託管 Runner,沒用本機 SSD 快取
- ☐ 16GB 機器同時跑多條 Job 或模擬器
- ☐ 改 README 也觸發了 iOS 全量構建
- ☐ Xcode 版本與本機不一致
勾了前三條,優先修快取;勾了後兩條,優先修觸發條件與機器規格。磁碟滿了也會越編越慢,可參考 Runner 磁碟清理文。
常見問題
GitHub Actions 一定比本機慢嗎?
不一定,但 Flutter iOS 流水線在快取無法升溫時,確實常出現顯著落差。自託管 Runner 通常能縮小差距。
要不要直接轉自託管 Runner?
先量測。若耗時表顯示固定冷啟成本,再挑一條 workflow 做 A/B 比較,連續 10 次中位數更有說服力。
一定要買高規硬體才有改善嗎?
通常不用。穩定快取的 Apple Silicon 租用 Mac,在 iOS 建置上常比無狀態大機更有效率。
怎麼說服團隊投入這件事?
把每階段省下的分鐘換算成等待成本,再加上發版可預測性提升,決策會更清楚。
專題延伸連結
若你正在重整 Flutter iOS CI,建議依序閱讀:
- 樞紐文:Flutter iOS CI 架構與決策地圖
- 快取專題:DerivedData、CocoaPods、SPM 實作
- 雲端 Mac 自託管 Runner 搭建(GitHub Actions)
- iOS CI 簽章與 Keychain 最小風險流程
- 長期 Runner 的磁碟與 inode 監控
想要穩定、可預測的 iOS CI 速度?
kvmboot 提供 Apple Silicon 雲端 Mac,適合 Flutter iOS 流水線。可先租用 Mac 日租壓測,再轉月租自託管 Runner。
若你想把 CI 從「每天碰運氣」變成可控系統, 查看雲端 Mac 方案