Swiftのビルド時間が遅い?開発効率を劇的に向上させてコンパイル待ちを減らす具体的な高速化テクニック5選

Swiftのビルド時間が高速化されコンパイル待ちから解放された開発者を表すイラスト。Xcodeの進捗バーと稲妻のイメージ プログラミング言語

「Build Succeeded」の表示を待つ間、ただ進捗バーを眺めている——Swiftでの開発を続けていると、誰もが一度はあの時間の長さに苛立ちを覚えるのではないでしょうか。
Swiftは強力な型推論を備えた、表現力の高い言語です。
しかし裏を返せば、コンパイラはその表現力の代償として、型の解決やモジュール間の依存解析といった重い処理を担っており、プロジェクトの成長とともにビルド時間は静かに膨らんでいきます。

見過ごされがちなのは、この待ち時間のコストが単なる足し算ではないという点です。
人間の集中は一度途切れると復元に時間を要するため、ビルド待ちは「フロー状態の破壊」として複利のように効いてきます。
仮に1回のビルドで2分、1日20回ビルドするとすれば、1日あたり40分。
月にすれば13時間超、年間では160時間近くに達します。
これは、本来コードを書くべき時間が静かに奪われているということです。

本記事では、その対策として、実プロジェクトで効果を検証してきた高速化テクニックを5つに絞って解説します。
扱うのは、ボトルネックの計測と特定、型チェックの最適化、ビルド設定の見直し、そして構造的な改善まで。
場当たり的な対処ではなく、まず現状を計測し、原因を切り分けたうえで効果の高い施策から適用していくという、論理的なアプローチを軸に据えています。

「コンパイル待ち」を減らすことは、すなわち開発効率を取り戻すことにほかなりません。
本記事をきっかけに、ビルドの完了を待つ時間を、本来の創造的な作業へと取り戻しましょう。

  1. Swiftのビルドが遅くなる原因とは?コンパイル時間が増大する仕組みを解説
    1. Swiftコンパイラのビルドパイプラインの基礎知識
    2. 型推論のコストがビルド時間に与える影響
    3. モジュール間の依存関係と再ビルド範囲の関係
  2. まずは計測から!Xcodeでビルド時間を可視化してボトルネックを特定する方法
    1. XcodeのBuild Timelineでビルド時間を分析する
    2. swiftcのタイムコンパイルオプションで処理時間を出力する
    3. xcodebuildとxcresulttoolsでビルド時間を数値管理する
  3. 【テクニック1】型注釈の追加で型推論の負荷を減らすコーディングの工夫
    1. 三項演算子やnil合体演算子が引き起こす型チェックの遅延
    2. 文字列連結の+演算子を補間に置き換えて高速化する
    3. 明示的な型注釈でコンパイラの推論コストを削減する
  4. 【テクニック2】Xcodeのビルド設定を見直してDebugビルドを高速化する
    1. Optimization LevelをNoneに設定する
    2. DebugビルドでdSYM生成を無効化する
    3. Build Active Architecture Onlyで対象アーキテクチャを絞る
  5. 【テクニック3】モジュール分割とアクセス修飾子でインクリメンタルビルドを活かす
    1. Swift Packageでローカルモジュールに分割する進め方
    2. privateやopenの使い分けで依存を最小化する
    3. 変更の影響範囲を狭める依存設計のポイント
  6. 【テクニック4】ビルドキャッシュと並列ビルドでコンパイル待ちを削減する
    1. Derived Dataのキャッシュ管理とクリアのタイミング
    2. TuistやBazelでキャッシュを共有する
    3. ビルドの並列数を最適化してマルチコアを活用する
  7. 【テクニック5】Xcode Previewsと開発環境の見直しでビルド回数を減らす
    1. Xcode PreviewsでUI確認のビルドを削減する
    2. CI/CDの分散ビルドとキャッシュ共有の活用
    3. メモリ・SSD・マシンスペックがビルド速度に与える影響
  8. まとめ:Swiftのビルド時間を高速化して開発効率を取り戻そう

Swiftのビルドが遅くなる原因とは?コンパイル時間が増大する仕組みを解説

Swiftのビルドが遅くなる原因を型推論とモジュール依存の観点から図解したイラスト

効果的な対策を講じるには、まず「なぜ遅くなるのか」を構造的に理解する必要があります。
Swiftのビルド時間は、単純にコードの行数に比例するわけではありません。
型推論の複雑さ、モジュール間の依存関係、コンパイラの内部処理といった複数の要因が絡み合い、ある閾値を超えたあたりから急激に増大するケースが少なくありません。
ここでは、ビルド時間が伸びるメカニズムを3つの観点から整理します。

Swiftコンパイラのビルドパイプラインの基礎知識

Swiftのコンパイル処理は、大きく分けてフロントエンドとバックエンドの2段階で構成されています。
フロントエンドではソースコードの構文解析、意味解析、そして型チェックが行われ、その結果は中間表現のSILへと変換されます。
バックエンドはLLVMをベースにしており、SILをさらに低水準の中間表現へ落とし込み、最適化を施したうえで機械語を生成する役割を担います。

さらに、最適化の戦略として「Whole Module Optimization(WMO)」と「インクリメンタルコンパイル」という2つのモードが存在します。
ReleaseビルドのデフォルトであるWMOは、モジュール全体を1つの単位として最適化できる一方、1ファイルの変更がモジュール全体の再コンパイルを引き起こします。
逆にDebugビルドで用いられるインクリメンタルモードは、変更されたファイルのみを再コンパイルするため高速ですが、最適化の余地は限られます。
このトレードオフを理解しているかどうかが、後述するビルド設定の見直しで効いてきます。

パイプラインの各段階とビルド時間への影響を整理すると、次の表のとおりです。

フェーズ 主な処理 ビルド時間への影響要因
フロントエンド 構文解析・型チェック・SIL生成 型推論の複雑さ、ファイル数
バックエンド LLVM最適化・機械語生成 最適化レベル(-O / -Onone)
リンク オブジェクトファイルの結合 依存モジュール数、フレームワーク数

型推論のコストがビルド時間に与える影響

Swiftの型システムは、Hindley–Milner型推論をベースに制約ソルバーで拡張された仕組みを採用しています。
開発者が型を明示しなくても文脈から型を導出できるのはこの恩恵ですが、裏を返せば、コンパイラは「この式に成立しうる型の組み合わせ」を制約として列挙し、解を探索していることになります。
そして探索空間は式の複雑さに対して単調に増えるのではなく、場合によっては指数関数的に膨らみます。
型推論の最悪計算量が指数時間になりうることは、Hindley–Milner系に古くから知られる理論的な性質です。

たとえば、整数と実数が混在した配列リテラルを見てみましょう。

// 各リテラルに候補型が割り振られ、制約ソルバーの探索空間が広がる例
let samples = [1, 2.5, 3, 4.5, 5]

各リテラルはIntにもDoubleにもなりうるため、コンパイラは要素間の整合性を確認しながら解を探索します。
式が短いうちはすぐに収束しますが、こうした式が長大な関数内に連なると、型チェックの時間は目に見えて伸びていきます。
ビルドログを確認した際に、特定のファイルだけコンパイル時間が突出している場合、その原因はほぼ間違いなく型推論の爆発だと考えてよいでしょう。

モジュール間の依存関係と再ビルド範囲の関係

ビルド時間のもう一つの大きな要因が、モジュール間の依存グラフです。
コンパイラは依存グラフに基づいて「どのファイルを再コンパイルすべきか」を決定しますが、この判断は依存関係の形状に強く左右されます。

たとえば、次のような構成を考えてみてください。

  • AppターゲットはFeatureAとFeatureBモジュールに依存
  • FeatureAとFeatureBはいずれもCoreモジュールに依存

この状態でCoreに1行でも変更を加えれば、それに依存するFeatureAとFeatureB、さらにはAppまでもが再ビルド対象になります。
つまり、下流の共通モジュールへの変更は、依存グラフを伝って上流全体へと波及するのです。

一方、依存を最小化する設計、たとえばprivateな実装の徹底や、変更頻度の高いコードと安定したコードの分離は、再ビルド範囲を局所化します。
Xcodeの依存判定はファイル単位・宣言単位で行われますが、アクセス修飾子によって「外部から観測されない変更」であることを示せば、再ビルドが不要と判断できる余地が生まれます。
パイプライン・型推論・依存関係という3層構造でビルド時間を捉えておくと、次章以降の計測と対策の切り分けが格段にやりやすくなるはずです。

まずは計測から!Xcodeでビルド時間を可視化してボトルネックを特定する方法

XcodeのBuild TimelineでSwiftのビルド時間を計測しボトルネックを分析する様子のイラスト

前章で見たとおり、ビルド時間の要因は型推論・依存関係・ビルド設定と多岐にわたります。
では、どこから手をつけるべきでしょうか。
答えは計測です。
ソフトウェア工学の格言に「推測するな、計測せよ」という言葉がありますが、ビルド時間の最適化でも例外ではありません。
体感では「とにかく遅い」としか分からない状況でも、計測によって「どのファイルのどの関数が重いのか」まで切り分ければ、対処の優先順位は自ずと定まります。
ここでは、粒度の異なる3つの計測手法を紹介します。

XcodeのBuild Timelineでビルド時間を分析する

もっとも手軽なのが、Xcode標準のBuild Timelineです。
確認手順は次のとおりです。

  1. ナビゲーターバーからReport Navigator(Cmd+9)を開く
  2. 一覧から最新のBuildレポートを選択する
  3. タイムライン上のバーを選択し、各タスクの所要時間を確認する

このタイムラインには、コンパイルやリンクといったタスクが時間軸に沿って表示されます。
注目すべきはCompile Swift Sourcesの各バーです。
特定のファイルだけバーが突出して長い場合、そのファイルで型推論が爆発している可能性が高い。
逆に全ファイルがまんべんなく遅いなら、ビルド設定やマシンスペックに原因があると考えられます。
この「局所か、全域か」の判定だけでも、次に打つべき手は大きく変わるはずです。

swiftcのタイムコンパイルオプションで処理時間を出力する

Build Timelineの分解能が「ファイル単位」だとすれば、次の手法は「関数単位」まで掘り下げられます。
ターゲットのBuild SettingsにあるOther Swift Flagsに、次の2つのフラグを追加してください。

-Xfrontend -debug-time-compilation
-Xfrontend -debug-time-function-bodies

この状態でビルドすると、各関数の型チェック時間がコンパイラログへ出力されます。
コマンドラインから実行すれば、重い順に並べ替えることも可能です。
ワークスペース名やスキームは、ご自身の環境に合わせて読み替えてください。

xcodebuild -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -configuration Debug \
  build 2>&1 | grep -E '^[0-9]+(\.[0-9]+)?ms' | sort -rn | head -20

出力の上位に三項演算子や+による文字列連結が多用された式が現れたら、型推論のボトルネックとほぼ断定できます。
なお、これらのフラグは計測専用です。
原因を特定したら外しておくのがよいでしょう。

xcodebuildとxcresulttoolsでビルド時間を数値管理する

1回きりの計測では、改善の効果を検証できません。
継続的な数値管理には、xcodebuildの-resultBundlePathとxcresulttoolを組み合わせます。

xcodebuild -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -configuration Debug \
  -showBuildTimingSummary \
  -resultBundlePath build/Build.xcresult \
  clean build

-showBuildTimingSummaryを付けると、ビルド完了時にフェーズ別の所要時間サマリーが出力されます。
さらに、保存した.xcresultは次のコマンドでJSONとして取り出せます。
古いXcodeでは--legacyオプションの指定が必要な点に留意してください。

xcrun xcresulttool get object --path build/Build.xcresult --format json

このJSONをjqなどでパースし、CIでビルドのたびに記録しておけば、施策の前後でビルド時間を定量的に比較できます。
「設定を見直した結果、Debugビルドが何秒短縮されたか」を数字で示せることは、チームで改善を進めるうえで強力な説得材料になるはずです。

最後に、3つの手法の使い分けを整理しておきます。

手法 計測単位 主な用途
Build Timeline タスク/ファイル単位 全体の俯瞰と原因の切り分け
debug-time系フラグ 関数単位 重いコードの特定
xcresulttool ビルド単位 CIでの継続的な履歴管理

まず全体を俯瞰し、怪しい箇所を関数単位で特定、そして数値として記録する。
この流れをルーティン化できれば、次章以降のテクニックを「何となく」ではなく「根拠をもって」適用できるようになります。

【テクニック1】型注釈の追加で型推論の負荷を減らすコーディングの工夫

型注釈を追加してSwiftの型推論エラーを解消しビルドを高速化するコード例のイラスト

前章の計測で「特定の関数の型チェックだけが突出して重い」と判明した場合、その原因の多くは、1つの式に演算子が連なり、複数のリテラルが混在したコードにあります。
前章で述べたとおり、Swiftの型推論は制約ソルバーによる探索です。
式が複雑になるほど制約の数と候補の組み合わせが増え、探索空間は指数関数的に広がります。
したがって対策の本質はシンプルで、コンパイラに渡す「未確定の情報」を減らすことです。
ここでは、すぐに実践できる3つの工夫を具体的に見ていきましょう。

三項演算子やnil合体演算子が引き起こす型チェックの遅延

三項演算子?:は、真偽のいずれの分岐であっても両辺の型を統一する必要があるため、制約ソルバーに「第2オペランドと第3オペランドの共通の型を求めよ」という制約を課します。
nil合体演算子??も同様で、Optionalのラップ構造を解きほぐすための型昇格の規則が絡んできます。
これらが1行に連なると制約同士が相互依存し、探索のバックトラッキングが多発します。

// 重い例: 演算子とリテラルが1つの式に連なる
let text = (count > 1 ? "items" : "item") + (total == 0 ? "" : " total")

短い式であれば一瞬で収束しますが、こうしたパターンが長い関数内で積み重なると、型チェック時間が秒単位で伸びることがあります。
対策は、1つの式に詰め込まず、部分式を変数に切り出して型を明示することです。

// 軽い例: 部分式を切り出し、型注釈を追加する
let suffix: String = count > 1 ? "items" : "item"
let text: String = total == 0 ? suffix : suffix + " total"

文字列連結の+演算子を補間に置き換えて高速化する

+による文字列連結が遅い理由は、+が多くの型に対してオーバーロードされた総称関数である点にあります。
コンパイラは各+について、オペランドの候補型とオーバーロードの候補を同時に解決しなければなりません。
一方、文字列補間\(...)は各値が独立した式として扱われるため、制約の連鎖が生じません。

// 重い例
let query = "SELECT * FROM users" + " WHERE age > " + String(age) + " ORDER BY name"

// 軽い例
let query = "SELECT * FROM users WHERE age > \(age) ORDER BY name"

補間への置き換えは、連結のたびに発生する文字列のアロケーションを減らせる点で、実行時の効率にも寄与します。
可読性の観点からも、明確に有利な書き換えだといえるでしょう。

明示的な型注釈でコンパイラの推論コストを削減する

もっとも汎用性の高い対策が、変数宣言への型注釈です。
型注釈は制約ソルバーにとって「この変数の型は最初から確定済み」という強力なヒントとなり、探索空間を根元から刈り込みます。
特にCGFloatやDoubleの算術、高階関数の戻り値を受け取る箇所では効果が顕著です。

// 重い例: 数値演算の型がすべて推論に委ねられている
let margin = blueRect.frame.width > 10 ? padding + spacing : padding * 2

// 軽い例: 幅を切り出し、結果の型を明示する
let width: CGFloat = blueRect.frame.width
let margin: CGFloat = width > 10 ? padding + spacing : padding * 2

型注釈を付けるべき箇所は、場当たり的ではなく次の基準で選定するとよいでしょう。

  • 数値リテラルを含む計算結果を受け取る変数
  • mapやfilterなど、総称的な高階関数の戻り値を受け取る変数
  • 1行に3つ以上の演算子が連なる式の途中結果

最後に、本テクニックで扱ったパターンを整理します。

パターン 型チェックが重くなる理由 対策
三項演算子の連結 両辺の型統一制約が相互依存する 部分式に切り出して型注釈を付与
nil合体演算子の連鎖 Optional昇格規則の解決コスト 同上
+による文字列連結 オーバーロード解決の候補爆発 補間に置き換える
混在する数値リテラル リテラル候補型の組み合わせ爆発 変数と型を明示する

書き方を少し整えるだけで、計測結果に数字として現れる——Swiftのビルド高速化において、このテクニックはコストパフォーマンスが最も高い施策のひとつです。
まずは計測で上位に現れた関数から、着手してみてください。

【テクニック2】Xcodeのビルド設定を見直してDebugビルドを高速化する

Xcodeのビルド設定画面で最適化レベルとdSYM生成の設定を変更する様子のイラスト

ビルド時間を「コードの複雑さ」と「設定の重さ」の積として捉えると、前章のコーディング工夫は前者へのアプローチ、本章のテーマは後者へのアプローチに相当します。
設定の見直しには、一度正せばコードベースの成長にかかわらず効果が持続するという利点があります。
一方で見落とされがちなのは、リリース用に調整した設定がDebug構成に混入したり、クラッシュレポートツールの導入手順に沿った結果、設定が書き換わったりする「意図しない変更」です。
そこで本章では、Debugビルドの速度に直結する3つの設定を、あるべき姿とともに確認していきます。
なお設定の運用にはxcconfigファイルの活用が有効です。
設定をテキストとしてGit管理すれば、差分レビューで変更を検知でき、意図しない書き換えを構造的に防げます。

Optimization LevelをNoneに設定する

第1章で解説したとおり、コンパイラのバックエンドはLLVMによる最適化パスを担います。
最適化レベルを-Ononeに設定すれば、このパスが大幅にスキップされ、コンパイル時間は顕著に短縮されます。
デバッグ構成の目的は動作検証であり実行性能ではないため、Debugビルドでは-Ononeが本質的に正しい選択です。
しかし実際には、Release用の設定を複製した際に最適化レベルが引き継がれていたり、古いテンプレートのままで運用されていたりするプロジェクトは少なくありません。
まずは現在の値を確認してみてください。

レベル 挙動 適した構成
-Onone 最適化なし・コンパイル最速 Debug
-O 実行速度重視の最適化 Release
-Osize バイナリサイズ重視の最適化 Release(サイズ重視)

あわせて確認したいのが、コンパイルモードのSWIFT_COMPILATION_MODEです。
第1章で触れたWhole Module Optimizationの構成(wholemodule)がDebugに適用されていると、1ファイルの変更がモジュール全体の再コンパイルを引き起こします。
Debugではincrementalモードを明示しておくのがよいでしょう。

// Debug.xcconfig
SWIFT_OPTIMIZATION_LEVEL = -Onone
SWIFT_COMPILATION_MODE = incremental

DebugビルドでdSYM生成を無効化する

dSYMはデバッグシンボルのバンドルで、クラッシュログ上のアドレスを関数名や行番号へ変換するシンボリケーションに用いられます。
問題は、DEBUG_INFORMATION_FORMATをdwarf-with-dsymに設定すると、リンク完了後にdSYM生成という追加フェーズが走る点です。
バイナリが大きくなるほどこの処理は重くなり、数十秒規模の待ち時間が発生することがあります。

なぜDebugでdwarf-with-dsymになっているのかをたどると、Crashlyticsなどのクラッシュレポートツール導入時のマニュアルに従ったケースが大多数です。
シンボリケーションはReleaseのアーカイブで必要になるものであり、Debugではオーガナイザのクラッシュ表示もdwarfで機能するため、生成を無効化しても実用上の問題は生じません。

// Debug.xcconfig
DEBUG_INFORMATION_FORMAT = dwarf

Build Active Architecture Onlyで対象アーキテクチャを絞る

iOSの実行環境は、実機のarm64、Apple SiliconとIntel Macのシミュレータそれぞれのアーキテクチャなど、複数のターゲットを持ちます。
ONLY_ACTIVE_ARCHをNOにしていると、コンパイルとリンクがアーキテクチャの数だけ繰り返されるため、処理負荷はほぼ線形に増加します。
YESに設定すれば、接続中のデバイスに対応する1つのアーキテクチャのみがビルド対象になります。

// Debug.xcconfig
ONLY_ACTIVE_ARCH = YES

注意点として、App Store配布用のアーカイブではユニバーサルバイナリの生成が求められるため、Release構成ではNOが標準です。
つまりこの設定は「DebugでのみYESにする」という構成が論理的に整合します。

最後に、本章で扱った設定を一覧にまとめます。

設定キー Debugの推奨値 効果
SWIFT_OPTIMIZATION_LEVEL -Onone バックエンド最適化のスキップ
SWIFT_COMPILATION_MODE incremental 変更ファイルのみ再コンパイル
DEBUG_INFORMATION_FORMAT dwarf リンク後のdSYM生成を省略
ONLY_ACTIVE_ARCH YES 対象アーキテクチャを1つに限定

コードに一切手を加えずに済む点で、本章のテクニックは導入コストがほぼゼロです。
まず現状の設定をxcconfigに書き出し、前章で構築した計測環境と組み合わせて、設定変更前後のビルド時間を定量的に比較してみてください。

【テクニック3】モジュール分割とアクセス修飾子でインクリメンタルビルドを活かす

Swift Packageでプロジェクトをモジュール分割し依存グラフを整理する構成図のイラスト

前章までのテクニックは、1回のビルド処理そのものを軽くする施策でした。
しかし第1章で解説した依存グラフの話を思い出してください。
ビルド時間の総量は「1回のビルドの重さ×再ビルドされる範囲」で決まります。
単一ターゲットで成長した大規模プロジェクトが、小規模なプロジェクトより桁違いに遅い原因は、しばしば後者、つまり構造にあります。
本章では、モジュール境界の設計とアクセス修飾子の使い分けによって、変更の影響範囲そのものを狭める手法を扱います。

Swift Packageでローカルモジュールに分割する進め方

単一ターゲットでは、コンパイラは宣言単位での依存追跡を行うものの、境界が曖昧です。
あるファイルで定義した型を多数のファイルが参照していれば、その型の変更は連鎖的な再コンパイルを招きます。
そこで有効なのが、Swift Packageによるローカルモジュール分割です。
モジュールを跨ぐ参照はpublicなインターフェース経由に限定されるため、モジュール内部の非公開な変更は外部へ波及しなくなります。
ビルドシステムはモジュール単位で成果物をキャッシュできるため、変更されていないモジュールの再コンパイルは丸ごと省略されるのです。

では、どの単位で切り出すべきでしょうか。
次の順序を推奨します。

  1. 依存の少ない安定したコード(ドメインモデル、ユーティリティ、通信層)から切り出す
  2. XcodeのFile > New > Packageでローカルパッケージを追加し、AppターゲットのFrameworksに紐づける
  3. 機能単位のFeatureモジュールを追加し、最後にAppターゲットを薄くする

Package.swiftの最小構成は次のとおりです。

// Package.swift
let package = Package(
    name: "CoreKit",
    platforms: [.iOS(.v16)],
    products: [
        .library(name: "CoreKit", targets: ["CoreKit"])
    ],
    targets: [
        .target(name: "CoreKit")
    ]
)

設計上の鉄則は、依存方向を一方向に保つことです。
モジュール同士が相互に参照する循環依存は、再ビルド範囲の局所化を根本から壊します。

privateやopenの使い分けで依存を最小化する

アクセス修飾子とビルド時間に、どんな関係があるのでしょうか。
鍵は、コンパイラとビルドシステムが宣言の可視性を「この変更は外部から観測可能か」の判定材料として使う点です。
privateな実装詳細の変更はファイル内で完結し、internalな変更はモジュール内に閉じます。
一方、publicやopenの変更は、依存するすべてのモジュールの再ビルドを誘発します。

修飾子 可視範囲 変更時の再ビルド範囲
private / fileprivate 同一ファイル内 そのファイルのみ
internal 同一モジュール内 そのモジュールのみ
public / open モジュール外も含む 依存するモジュール全体

実践上の指針は、最小の可視性から始め、必要になったときにだけ広げるです。

public final class UserRepository {
    // 実装の詳細はすべてprivateに。ここを書き換えても下流は再ビルドされない
    private var cache: [String: User] = [:]

    public func find(id: String) -> User? {
        cache[id]
    }
}

特別な注意が必要なのがopenです。
openはモジュール外からの継承とオーバーライドを許すため、コンパイラは「呼び出し先は実行時に差し替わりうる」という最も広い想定での扱いを強いられます。
拡張点として意図的に公開する場合に限定すべきでしょう。

さらに効果が大きいのが、Swift 6で導入されたinternal importです。
ライブラリへの依存を実装の詳細として宣言することで、その依存が公開APIへ漏れるのを型システムレベルで防げます。

// Swift 6以降: このパッケージの公開APIは、Alamofireに依存しないことを宣言する
internal import Alamofire

下流モジュールから見れば「Alamofireに依存しないモジュール」となるため、依存ライブラリのアップデートが下流の再ビルドを誘発しなくなります。

変更の影響範囲を狭める依存設計のポイント

最後に、モジュール分割と修飾子の使い分けを支える設計原則を整理します。

  • 変更頻度でモジュールを分類する。安定したCoreと、変動の激しいFeatureを分離し、依存は必ず不安定な側から安定した側へ向ける
  • 全モジュールから参照される巨大な共通ファイルを避ける。Constants.swiftやExtensions.swiftのようなファイルは、1行の変更が全域再ビルドの起点になる
  • 抽象に依存させる。プロトコルを境界に置けば、実装の差し替えが上流へ波及しにくい

こうして見ると、この原則は安定依存の原則や依存性逆転の原則と同じ方向を指しています。
つまり、保守性のために推奨されてきたアーキテクチャの原則には、ビルド時間の観点からも合理的な根拠があるのです。
疎結合な設計は可読性やテスト容易性を高めるだけでなく、コンパイラの仕事量を物理的に減らす。
この認識を持っておくと、アーキテクチャへの投資判断も説得力をもって行えるはずです。

【テクニック4】ビルドキャッシュと並列ビルドでコンパイル待ちを削減する

ビルドキャッシュと並列ビルドでSwiftのコンパイル待ち時間が短縮される様子のイラスト

ここまでのテクニックは、1回のビルドで行うべき計算量を減らすアプローチでした。
本章では発想を変え、一度計算した結果を捨てないというアプローチを扱います。
コンパイルは、入力(ソースファイルとその依存物)から出力(オブジェクトファイル)を得る純粋な関数とみなせます。
入力が変わらなければ出力も変わらない。
この性質を利用したのがビルドキャッシュであり、入力のハッシュを検索キーとする仕組みは、関数型プログラミングにおけるメモ化と本質的に同じです。
もう一つの柱が並列実行です。
ビルドシステムはタスクを依存グラフ(DAG)として管理し、互いに依存しないタスクを複数のコアへ振り分けます。
つまり本章の主題は、キャッシュによる再計算の省略と、並列化による待ち時間の圧縮です。
まず、キャッシュが効く範囲を3つの層で整理しておきます。

キャッシュの層 保持場所 主に効く場面
インクリメンタルビルド 各自のMac 日常の反復ビルド
モジュールキャッシュ Derived Data内 依存パッケージの再コンパイル回避
リモートキャッシュ チーム・CIで共有 クリーンな環境での初回ビルド

Derived Dataのキャッシュ管理とクリアのタイミング

Derived Dataは、中間ビルド成果物やインデックス、モジュールキャッシュが格納されるディレクトリです。
ビルドのたびにXcodeはここで「前回と何が変わったか」を照合し、変更のないモジュールの再コンパイルを省略します。

一方で解説記事を見ると、「ビルドがおかしい→Clean Build Folder(Shift+Cmd+K)」という処方箋が定番化しています。
そもそも、なぜCleanで不調が直ることがあるのでしょうか。
それは、モジュールキャッシュの不整合が解消されるためです。
しかしCleanはキャッシュを意図的に破棄し、フルビルドと同じ状態へ戻す操作でもあります。
頻発するクリーンは、キャッシュの恩恵を自ら放棄する行為だと捉えるべきです。
クリアすべきは、キャッシュの内容が実際の入力と不一致を起こしたときだけ。
代表的なタイミングは次のとおりです。

  • XcodeやSwiftツールチェーンのアップデート直後(モジュールキャッシュの形式が変わるため)
  • 依存パッケージ更新後に「No such module」などの不可解なエラーが出続ける場合
  • 大規模なブランチ切り替え後、明らかに古い状態でビルドされている場合

これらに該当しないなら、Derived Dataは保護すべき資産です。
クリアする場合も、プロジェクト単位に限定するのが賢明でしょう。

# Derived Dataの容量を確認
du -sh ~/Library/Developer/Xcode/DerivedData

# 特定プロジェクトのキャッシュのみ削除
rm -rf ~/Library/Developer/Xcode/DerivedData/MyApp-*

TuistやBazelでキャッシュを共有する

ローカルキャッシュには構造的な限界があります。
第一に、キャッシュはマシンごとに独立しているため、新しいメンバーが参加した日やCIのクリーン環境では、誰かがすでに計算したはずの処理をもう一度実行します。
第二に、チームという単位で見れば、同じ入力を人数分だけ繰り返し計算する非効率が残ります。
この問題への答えがリモートキャッシュです。
各ターゲットの入力ハッシュをキーに成果物を共有サーバーへ保存し、同一の入力がどこかでビルド済みなら、その成果物をダウンロードして再利用します。
CIでビルドした成果物を、メンバーのローカル初回ビルドが拾えるイメージです。

実現手段の代表がBazelです。
rules_xcodeprojと組み合わせれば、プロジェクトの生成からビルドまでをBazelに統合でき、リモートキャッシュは設定ファイルへの数行で有効化できます。

# .bazelrc: チーム共有のリモートキャッシュを設定
build --remote_cache=grpc://cache.internal.example.com:9092

TuistはXcodeprojの生成をコード化してチーム内の差異をなくすツールで、ターゲット単位のキャッシュ機構も提供しています。
プロジェクト構成の再現性とキャッシュを同時に扱える点が特徴です。
いずれも移行コストは見過ごせませんが、前章のモジュール分割と相性がよい点は共通しています。
モジュール境界が明確なほどキャッシュの単位は細かくなり、ヒット率は上がります。
構造の整備が済んだプロジェクトほど、キャッシュ共有の投資回収は早いのです。

ビルドの並列数を最適化してマルチコアを活用する

並列化の効果には理論的な上限があります。
依存グラフの中で最も長い直列の経路、すなわちクリティカルパス以上には短縮できず、並列化できない区間の割合が全体の効果を律速します。
これはAmdahlの法則そのものです。
最終的なリンク処理やWhole Module Optimization下のコンパイルは直列区間になりやすく、ここが太いならコアを増やしても効果は頭打ちになります。
前章のモジュール分割が並列化にも寄与するのは、依存グラフの幅を広げ、同時に実行できるタスクを増やすからです。

並列数の制御は、コマンドラインでは-jobsで行えます。

# 並列ビルド数を8に制限
xcodebuild -project MyApp.xcodeproj -scheme MyApp -jobs 8 build

デフォルトの並列数は搭載コア数に準じますが、並列数を上げれば常に速いわけではない点に注意してください。
swiftcの1プロセスは数GBのメモリを消費しうるため、メモリの少ない環境で並列数を増やすとスワップが発生し、むしろ遅くなります。
環境ごとの指針をまとめます。

環境 推奨並列数 根拠
RAMに余裕のあるローカルMac 既定(コア数) スループットの最大化
RAM 16GB以下のMac コア数より抑制 スワップの回避
CIのクリーン環境 コア数+リモートキャッシュ併用 コールドキャッシュ対策

キャッシュと並列化は、「計算資源を無駄にしない」という同じ原則の別の側面です。
計測で把握したビルド時間の内訳に対して、再計算が起きていないか、直列区間が太くないかを問うてみてください。
次章は視点を変え、そもそものビルド回数を減らすアプローチを扱います。

【テクニック5】Xcode Previewsと開発環境の見直しでビルド回数を減らす

Xcode Previewsと快適な開発環境でSwiftのビルド回数を減らす様子のイラスト

ここまでの4つのテクニックは、すべて「1回のビルドの重さ」を減らす方向の施策でした。
しかし冒頭で示したモデルを思い出してください。
ビルド待ち時間の総量は「1回の重さ×回数」で決まります。
重さをどれだけ削っても、回数が減らなければ総量の下限は思うように下がりません。
しかも開発中のビルドを振り返ると、相当数はロジックの動作確認ではなく、UIの見た目確認のために発生しています。
本章では、回数そのものを減らすXcode Previews、フィードバック待ちをチーム規模で最適化するCI/CD、そしてすべての土台となるハードウェアという、3つの層を扱います。

Xcode PreviewsでUI確認のビルドを削減する

まず、従来のUI確認のループを分解してみましょう。

  1. コードを変更する
  2. アプリをビルドしてシミュレータへインストールする
  3. 対象画面までナビゲーションする
  4. 見た目を確認し、修正点があれば1へ戻る

このループには、アプリ全体のビルドと起動、画面遷移という固定コストが毎回乗ります。
レイアウトの余白を1pt調整したいだけでもアプリ全体が再ビルドされるのは、計算量の観点から明らかに過剰です。

Xcode Previewsはこのループを根本から組み替えます。
プレビューは対象のViewとその依存関係のみを最小構成でコンパイルし、アプリを起動することなくキャンバスへ描画します。
変更から反映までが秒単位になり、ビルドが軽くなるだけでなく、ビルドと起動というステップそのものがループから消える点が本質です。

#Preview("ライト・大文字サイズ") {
    ProfileView(user: .mock)
        .preferredColorScheme(.light)
        .environment(\.dynamicTypeSize, .xxxLarge)
}

活用の鍵は、プレビューを「状態のカタログ」として整備することです。

  • モックデータを用意し、実データの取得処理に依存させない
  • ダークモードやダイナミックタイプなど、確認すべきバリエーションを列挙する
  • 空の状態・少数・大量データなど、表示パターンを網羅する

CI/CDの分散ビルドとキャッシュ共有の活用

ローカルのビルド回数を減らすのと並行して、チームレベルのフィードバック待ちも見直すべきです。
プルリクエストの検証に時間のかかるCIは、マージ待ちのタスクを溜め込ませ、検証を急ぐ開発者にローカルでの動作確認を促します。
つまりCIの遅さは、ローカルビルドの回数を増やす二次被害として跳ね返ってくるのです。

対策の第一は、CIでも前章のキャッシュ原則を適用することです。
SwiftPMの依存解決とコンパイルを毎回やり直すのは、入力が変わっていない以上、純粋な再計算の無駄です。
GitHub Actionsであれば、依存キャッシュを次のように永続化できます。

# GitHub Actions: SwiftPMキャッシュの永続化
- uses: actions/cache@v4
  with:
    path: ~/Library/Caches/org.swift.swiftpm
    key: spm-${{ hashFiles('Package.resolved') }}

第二は、テストの分散実行です。
Xcode Cloudは依存のキャッシュを標準で行い、テストを複数マシンへ振り分ける構成も選択できます。
さらに進んで、前章で触れたリモートキャッシュをCIとローカルで共有すれば、CIがビルドした成果物をメンバーの初回ビルドが再利用する形に到達できます。
キャッシュの原則をローカルとCIで一貫させること。
これがチーム全体のビルド時間を押し下げる構図です。

メモリ・SSD・マシンスペックがビルド速度に与える影響

最後に土台の話をします。
ソフトウェア側をどれだけ最適化しても、計算は最終的に物理的なハードウェア上で実行されます。
ボトルネックになりやすい3つの要素を整理しましょう。

要素 ボトルネックになる場面 選定の目安
CPU 型チェック・最適化パスの計算 コア数とシングルコア性能のバランス
メモリ swiftcの並列プロセス 16GB以上、並列数に応じて増量
SSD 中間成果物の読み書き NVMe規格の内蔵ストレージ

メモリは第4章の並列数の議論と直結します。
swiftcの並列プロセスはメモリを大量消費するため、RAMが逼迫してスワップが発生すれば、コア数を増やしても速度はむしろ悪化します。
機種の選定や増設では、コア数とメモリ容量を必ずセットで評価してください。

SSDは地味に見えて重要です。
インクリメンタルビルドは、キャッシュヒットの判定のために大量の中間ファイルを読み書きします。
I/O負荷が高いため、ストレージ速度はそのまま待ち時間へ反映されます。
キャッシュを活かす施策ほどI/Oの速さが効くのは、論理的な帰結といえるでしょう。

CPUについては、Amdahlの法則で述べたクリティカルパス、すなわち並列化できない直列区間の速度はシングルコア性能で決まります。
Apple Siliconへの移行でビルドが大幅に短縮された報告が多いのは、高いメモリ帯域とシングルコア性能が、直列区間を含めて効いているためです。

以上が5つのテクニックの最後となります。
重さの削減、範囲の局所化、キャッシュ、そして回数の削減と、あらゆるレイヤーの施策を揃えれば、あとは計測結果をもとに効果の高いものから適用していくだけです。
最終章では、本記事の内容を全体として振り返り、実践の優先順位を整理します。

まとめ:Swiftのビルド時間を高速化して開発効率を取り戻そう

ビルド時間が短縮され快適にSwift開発を楽しむエンジニアのイラスト

本記事では、Swiftのビルド時間という問題を、仕組みの理解と計測を軸に、5つのテクニックへ整理してきました。
最後に全体を振り返り、「何を、どの順で」着手すべきかを論理的に整理します。

記事を貫いてきたモデルを思い出してください。
ビルド待ち時間の総量は、「1回のビルドの重さ × ビルドの回数」で決まります。
5つのテクニックをこの式のどこに効くかで分類すると、全体像は次の表のとおりです。

テクニック 対象となる要素 効果の現れ方
型注釈などのコーディング工夫 制約ソルバーの探索空間 重い関数の型チェック時間が短縮
ビルド設定の見直し 最適化・dSYM・アーキテクチャ 1回あたりのビルド時間が一律に短縮
モジュール分割とアクセス修飾子 依存グラフと再ビルド範囲 変更のたびの再コンパイルが局所化
キャッシュと並列ビルド 再計算の省略と計算資源 クリーン環境や依存更新時の待ちが減少
Previewsと開発環境 フィードバックループの回数 UI確認目的のビルド自体が消滅

では、実践の順序はどう組むべきでしょうか。
基準は「導入コストの低さ × 効果の確実さ」です。
推奨する順序は次のとおりです。

  1. 計測環境を整える。Build Timelineとxcresulttoolで、現状の数値とボトルネックの位置を把握する
  2. ビルド設定を見直す。コードに一切手を加えず効果が出る、投資対効果が最も高い一手
  3. 計測で上位に現れた関数から、型注釈や補間への書き換えを進める
  4. 成長の見込みがあるなら、モジュール分割へ着手する。初期投資は必要だが、コードベースの成長に合わせて効果が複利で効く
  5. チーム全体の課題として、キャッシュ共有とCI連携、Previewsの整備を展開する

一つだけ、留意点を挙げておきます。
すべてを一度に実施する必要は、まったくありません。
むしろ計測なき最適化は、推測による改善と同じで、労力の割に効果が検証できないリスクを孕んでいます。
型注釈の過剰な付与はコードを冗長にしますし、モジュール分割のしすぎは管理コストを増やします。
数値として観測された問題に、数値で効果を確認しながら対処する。
この規律を保つことこそが、場当たり的な高速化との決定的な違いです。

最後に、冒頭の数値を思い出してください。
1回2分のビルドが1日20回积もれば、年間160時間近くに達する——開発者の集中が途切れ、フロー状態が破壊される時間です。
ビルド時間の短縮は、単なる作業時間の節約ではありません。
思考が途切れずにコードへ注がれる状態を守り、開発という知的活動の質を取り戻すための投資です。
明日から、まずは計測から始めてみてください。
進捗バーを眺める時間が、コードを書く時間に戻っていくはずです。

コメント

タイトルとURLをコピーしました