ソフトウェア設計

[モジュール化 #5] Xcodeのビルドが遅い理由と解決策(計測ツール付き)

コードを1行直してビルドボタンを押したら、数分待たされた経験はiOS開発者なら誰にでもあるでしょう。

読了 6 分
[モジュール化 #5] Xcodeのビルドが遅い理由と解決策(計測ツール付き)のカバー画像

コードを1行直してビルドボタンを押したら、数分待たされた経験はiOS開発者なら誰にでもあるでしょう。

1日に数十回ビルドするとすれば、1回1分の短縮で、誇張なく1日30分以上を取り戻せます。

結論から言うと、Xcodeビルドが遅い最大の構造的原因は、少しの変更でも再コンパイル範囲が広すぎることです。モジュール化はその範囲を直接縮小します。

この記事では、ビルドが遅くなる原因を層ごとに整理し、モジュール化の効果が及ぶ場所と、改善前後を数値で計測する方法までまとめます。

重要なポイントをまとめます。

  1. インクリメンタルビルドの再コンパイル範囲はモジュール単位に閉じます。モジュールがなければアプリ全体が1つの単位です。
  2. 計測せずにチューニングしてはいけません。まず使うべき基本ツールはXcodeのBuild With Timing Summaryです。
  3. モジュール化以外にも、型推論のボトルネックやdSYM設定など局所的な原因を確認する必要があります。

なぜXcodeのビルドは遅くなるのか?

原因は3つの層に分けて考えられます。

1つ目は再コンパイル範囲です。

Swiftではファイルが1つ変わると、そのファイルが属するモジュール内で影響を受けるファイルが再コンパイルされます。モジュール化されていないアプリでは、ターゲット全体が1つのモジュールです。

数十万行の単一ターゲットでは、些細な修正でも広範囲の再コンパイルを引き起こします。多くの場所で使われる型に触れると、ほぼクリーンビルドになります。

2つ目は型チェックのボトルネックです。

Swiftコンパイラは型推論に多くの時間を使います。複雑な式1つに数秒かかることもあります。長いチェーン、複雑な三項演算子、大きなSwiftUIのbodyが典型例です。

3つ目はビルド設定です。

デバッグビルドでdSYMを生成していたり(DWARF with dSYM File)、Whole Module Optimizationが誤って有効になっていたりすると、毎回不要なコストが加わります。


モジュール化はどのようにビルド速度を改善するのか?

モジュール化が改善するのは、1つ目の層である再コンパイル範囲です。

モジュール境界は再コンパイルの防火壁です。モジュール内部の実装だけが変わった場合、外部を再コンパイルする必要はありません。

前回の記事のレイヤー構造が、ここで力を発揮します。

  • 検索機能(FeatureSearch)の内部を変更すると、再コンパイルはFeatureSearchとアプリターゲット程度で済みます。
  • 一方、全員が依存する下位モジュール(CoreNetwork)の公開インターフェースを変更すると、上位層が次々に再コンパイルされます。

そのため、ビルド速度の観点からモジュール設計の原則が導けます。

  1. 頻繁に変わるコードはグラフの上流(Feature)に、安定したコードは下流(Core・Domain)に置きます。
  2. 下流モジュールのpublicインターフェースは最小限に保ちます(publicでなければ外部に影響しません)。
  3. 全員が依存する肥大化したCommonモジュールは作りません。

並列化の効果もあります。依存関係のないモジュールはXcodeが同時にコンパイルするため、幅が広く浅いグラフほどクリーンビルドも速くなります。

モジュール境界があれば、再コンパイルはこの範囲で終わります
モジュール境界があれば、再コンパイルはこの範囲で終わります

ビルド時間はどう計測するのか?

勘に頼った最適化は、たいてい外れます。3つのツールで十分です。

Build With Timing Summary。 XcodeのProduct → Perform Action → Build With Timing Summaryを実行すると、ビルドログの末尾に各段階の所要時間が整理されます。まずここで、どのターゲットと段階がボトルネックか確認してください。

型チェック警告フラグ。 時間のかかる関数や式をコンパイラが直接知らせます。Other Swift Flagsに追加します。

-Xfrontend -warn-long-function-bodies=100
-Xfrontend -warn-long-expression-type-checking=100
// 100ms 制限時間を超える関数・式に警告を出す

ビルドタイムライン。 ビルドログのAssistant領域でタイムラインを開くと、ターゲットごとの並列実行状況を確認できます。直列に長く並ぶ区間があれば、依存グラフが並列化を妨げているサインです。


モジュール化以外に確認すべきこと

計測すると、モジュール化より設定1行が問題だったということも少なくありません。デバッグビルド向けのチェックリストです。

項目 推奨設定(Debug)
Debug Information Format DWARF(dSYM生成を無効化)
Compilation Mode Incremental
Optimization Level -Onone
Build Active Architecture Only Yes

Xcode 16からは、明示的なモジュールビルド(Explicitly Built Modules)が導入され、モジュール準備段階の並列化も改善されました。最新のXcodeを使うこと自体がビルド速度の改善になります。

面接ではこう聞かれます

Q. 大規模なiOSアプリでビルド時間を短縮するアプローチを説明してください。

まずBuild With Timing Summaryでボトルネックを計測し、設定(デバッグ時のdSYM生成、コンパイルモード)を確認します。構造面では、モジュール化で再コンパイル範囲をモジュール内に閉じ込め、頻繁に変わるコードを依存グラフの上流に置き、下流モジュールのpublicな表面積を最小化することが重要です。

Q. モジュールを分割してもビルドが遅いままなら、何を疑いますか?

全員が依存する下位モジュールが頻繁に変わる構成か、publicインターフェースの変更が多いかを確認します。さらに警告フラグで型チェックのボトルネックを探して複雑な式を分解し、依存グラフが直列化されて並列コンパイルを妨げていないかビルドタイムラインで確認します。

ビルドを待つ時間は、1日分まとめると意外に大きくなります
ビルドを待つ時間は、1日分まとめると意外に大きくなります

ビルド速度はモジュール化の最も実感しやすい報酬ですが、モジュールが数十個になると、今度はプロジェクト管理自体が仕事になります。

シリーズ最終回では、この管理コストを下げるツールTuistを扱います。pbxprojの競合からの解放とバイナリキャッシュもあわせて整理します。