
Measure before you change a build setting. Teams that tune Xcode build times without profiling usually optimize files that account for a tiny fraction of the total while the real culprit sits untouched. This guide instruments an xcodebuild run, extracts the slowest Swift compile units, and applies targeted fixes — with a short shell script that turns raw log output into a sortable top-10 table.
profile on a Figment M-series Mac (requests from $95/month; exact chip, RAM and disk are confirmed in the quote). A $29 virtual Intel macOS VM (2 vCPU, 8 GiB RAM, 80 GiB) is a legitimate target for lightweight Intel toolchains and for running Xcode 26 or earlier on Intel, but Xcode 27, Apple-silicon-only simulators and parallel Swift compilation at scale belong on an M-series host. The flags and parser below are documented against Apple's guidance in Improving the speed of incremental builds.
Two layers of timing data combine well. Use both on a full clean build; incremental builds skip unchanged files and distort per-file numbers.
Layer 1 — the build phase summary:
xcodebuild build \
-scheme "$SCHEME" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-showBuildTimingSummary \
2>&1 | tee build-timing.log
Capture with tee so you can parse it offline. The summary prints at the end and groups wall-clock seconds by phase: CompileSwiftSources, CompileC, PhaseScriptExecution, CodeSign, Ld. That tells you which phase is the bottleneck before you drill into files.
Layer 2 — Swift frontend per-file timers, surfaced through OTHER_SWIFT_FLAGS:
xcodebuild build \
-scheme "$SCHEME" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-showBuildTimingSummary \
OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies -Xfrontend -debug-time-compilation" \
2>&1 | tee build-timing-detailed.log
| Flag | What it measures |
|---|---|
-Xfrontend -debug-time-function-bodies | Type-check time per function body |
-Xfrontend -debug-time-compilation | Frontend phase time per source file |
-Xfrontend -warn-long-function-bodies=100 | Warning when a function body takes over 100 ms to type-check |
Replace "$SCHEME" with the scheme name your project actually defines (from xcodebuild -list). Do not hard-code a scheme you have not verified.
A typical -showBuildTimingSummary section looks like:
Build Timing Summary
CompileSwiftSources (2 tasks) 47.3 seconds
CompileC (12 tasks) 3.1 seconds
PhaseScriptExecution (3 tasks) 8.4 seconds
CodeSign (1 task) 0.9 seconds
Ld (2 tasks) 1.2 seconds
Here Swift compilation is roughly 78% of total build time (47.3 of 60.9 s), which is where to focus. If PhaseScriptExecution is a large fraction, audit your run script build phases; scripts without declared input and output file lists invalidate incremental state and rerun every build.
The script below reads a log captured with tee, pulls CompileSwift lines that carry a file path and duration, sorts descending, and prints the top N. It fails fast and does not count failed builds as timings.
#!/usr/bin/env bash
# ci/parse-build-times.sh
# Usage: bash ci/parse-build-times.sh build-timing-detailed.log [N]
set -euo pipefail
LOG_FILE="${1:?Usage: $0 <xcodebuild-log-file> [top-N]}"
TOP_N="${2:-10}"
if [[ ! -f "${LOG_FILE}" ]]; then
echo "ERROR: log file not found: ${LOG_FILE}" >&2
exit 1
fi
# Confirm the captured build succeeded. A failed build has misleading timings.
if grep -qE "^\*\* BUILD FAILED \*\*|xcodebuild: error:" "${LOG_FILE}"; then
echo "ERROR: ${LOG_FILE} contains a failed build; refusing to report timings." >&2
exit 2
fi
echo "==> Top ${TOP_N} slowest CompileSwift files in ${LOG_FILE}"
awk '
/^CompileSwift / {
for (i = 1; i <= NF; i++) if ($i ~ /\.swift$/) { path = $i }
match($0, /\(([0-9]+\.[0-9]+) seconds\)/, m)
if (path != "" && m[1] != "") printf "%s\t%s\n", m[1], path
path = ""
}
' "${LOG_FILE}" \
| sort -rn \
| head -n "${TOP_N}" \
| awk 'BEGIN{printf "%-10s %s\n","SECONDS","FILE"} {printf "%-10s %s\n",$1,$2}'
Run it on demand, not on every CI push — the diagnostic flags themselves add measurable overhead.
bash ci/parse-build-times.sh build-timing-detailed.log 10
Once the top-10 list is in hand, look for these patterns:
Complex type inference. A long fluent chain like items.filter { ... }.map { ... }.reduce(0) { ... } forces the solver to infer every intermediate type. Split it:
let active: [Item] = items.filter { $0.isActive }
let values: [Int] = active.map { $0.value }
let total: Int = values.reduce(0, +)
Large files with heavy generics. Files over roughly 1,000 lines with nested generics or protocol extensions with associated types are reliable hot spots. Split them into smaller translation units.
Unconditional run script phases. A phase that runs pod install or SwiftLint on every build without declared input and output file lists invalidates the build graph. Declare the files so Xcode can skip unchanged runs.
Whole Module Optimization in Debug. SWIFT_COMPILATION_MODE = wholemodule is a Release setting; it maximises optimisation at the cost of single-threaded compilation. Confirm Debug is incremental in your .xcconfig or build settings pane.
| Fix | Effort | Where it helps |
|---|---|---|
| Explicit types on hot files | Low | Files in the top-10 with long solver time |
| Split files > 800 lines with heavy generics | Medium | Largest files in top-10 |
| Input/output file lists on run scripts | Low | Builds with 3+ run script phases |
Debug SWIFT_COMPILATION_MODE=incremental | Very low | Projects that inherited wholemodule into Debug |
| Clang module caching | Medium | Mixed Swift/Obj-C projects |
Use parse-build-times.sh before and after each change on the same host and scheme to measure the actual delta. Numbers reported by other teams on other hardware are not your baseline.
Build-time results are only comparable on identical hardware with the same caches warm. A hosted Mac that keeps DerivedData, the SwiftPM .build directory and the signing keychain on one disk between runs makes trend tracking meaningful. Caches to preserve between runs:
~/Library/Developer/Xcode/DerivedData keyed by scheme and git ref..build directory for SwiftPM dependency artifacts.Avoid xcodebuild clean except when explicitly testing a cold build.
Treat profiling as a short, bounded project and price it against a hosted Mac term. The arithmetic below uses verified Figment entry prices and is independent of any benchmark claim.
$95 × 3 = $285. The exact chip, RAM and disk at that entry are confirmed in the quote; do not assume parity with a specific competitor SKU.$249/month: $249 × 12 = $2,988. Includes one M-series Mac, CI setup, backups, monitoring and 30 minutes of monthly support.$999/month: $999 × 12 = $11,988. Includes two M-series Macs, two CI runners, team setup, backups, monitoring and one hour of monthly support.Match the plan to the specific CI work. A profiling spike that produces the top-10 list and two fixes rarely needs the team plan; a permanent shared runner usually does.
See Mac hosting plans at /mac-hosting or start a configuration at /mac-hosting/start.
Related guides: