A routine-looking feature merge can suddenly add more than ten megabytes to an archive, even when the commit history contains neither large images nor an obvious new dependency. If the IPA is checked manually only before release, the problem has usually spread across several branches by then. A more reliable approach is to preserve a size baseline in a fixed cloud Mac build environment, measure the App, main executable, and dynamic frameworks separately after every archive, and block the merge whenever the threshold is exceeded.
Define Comparable Metrics First
“Package size” can refer to at least three different measurements. They should not be plotted on the same trend line.
| Metric | What to measure | Primary purpose |
|---|---|---|
| Uncompressed App size | The .app directory inside the .xcarchive |
Detect growth in resources, frameworks, and localization files |
| Main executable size | The Mach-O file referenced by CFBundleExecutable |
Detect changes in code, static libraries, and symbols |
| IPA size | The exported compressed file | Track the size users actually download |
The gate should primarily use the first two metrics. IPA size is affected by compression ratios and file ordering, so even small changes to the same set of resources can amplify or reduce the compressed result. Debug symbols reside in the archive’s dSYMs directory and should not count toward the installed package, but they should be archived separately for later investigation.
Size differences are meaningful only when the build conditions are identical. The Xcode path, configuration, target platform, export method, and compiler settings must all remain fixed.
Create the Archive with a Fixed Command
Resolve dependencies in a clean working directory, then create a Release archive for a physical-device target. Pass the workspace name and Scheme as job parameters so that project-specific details are not hard-coded in the script.
set -euo pipefail
WORKSPACE="${WORKSPACE:?missing WORKSPACE}"
SCHEME="${SCHEME:?missing SCHEME}"
OUT="${OUT:-$PWD/build-size}"
ARCHIVE="$OUT/App.xcarchive"
rm -rf "$OUT"
mkdir -p "$OUT"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
clean archive \
CODE_SIGNING_ALLOWED=NO
If the project must run signing-related scripts during archiving, do not force signing off. Use the project’s existing controlled configuration instead. The gate is meant to stay consistent with the production build, not to produce one command that works for every repository. During initial adoption, also verify that the Release configuration does not accidentally include test resources, diagnostic libraries, or development server addresses.
Measure the App, Executable, and Frameworks Separately
The following script locates the single App inside the archive, reads the name of its main executable, and emits machine-readable TSV. du -sk is suitable for tracking directory usage over time, while stat provides the main executable’s exact size in bytes.
set -euo pipefail
ARCHIVE="${1:?usage: measure.sh path/to/App.xcarchive}"
APP_ROOT="$ARCHIVE/Products/Applications"
APP_PATH="$(find "$APP_ROOT" -maxdepth 1 -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
EXECUTABLE="$(/usr/libexec/PlistBuddy \
-c 'Print :CFBundleExecutable' "$APP_PATH/Info.plist")"
printf "kind name bytes
"
APP_KB="$(du -sk "$APP_PATH" | awk '{print $1}')"
printf "app %s %s
" "$(basename "$APP_PATH")" "$((APP_KB * 1024))"
printf "executable %s %s
" "$EXECUTABLE" \
"$(stat -f '%z' "$APP_PATH/$EXECUTABLE")"
FRAMEWORKS="$APP_PATH/Frameworks"
if test -d "$FRAMEWORKS"; then
find "$FRAMEWORKS" -maxdepth 1 -type d -name '*.framework' -print0 |
while IFS= read -r -d '' item; do
kb="$(du -sk "$item" | awk '{print $1}')"
printf "framework %s %s
" "$(basename "$item")" "$((kb * 1024))"
done
fi
Save the report as a build artifact instead of printing only a single total. When a regression occurs, reviewers can immediately determine whether the growth came from the main executable, a specific framework, or resources elsewhere in the App directory.
Break Down Resource Directories Further
If the total App size increases while the main executable and frameworks remain stable, run stat or du separately on Assets.car, localization directories, offline data files, and media resources. Do not immediately delete files that appear to be duplicates. First confirm whether they were generated by different targets, on-demand resource rules, or localization workflows.
Reduce False Positives with Dual Thresholds
A percentage-only threshold makes small components overly sensitive, while a fixed byte threshold can miss sustained growth in a large App. The permitted increase can be defined as:
allowed = max(8 MiB, baseline_app_bytes × 3%)
failed = current_app_bytes - baseline_app_bytes > allowed
The 8 MiB and 3% values are only starting points for adoption, not universal standards. Record the variation across several normal archives before tuning them for the project. The main executable and each individual framework should use smaller independent thresholds; otherwise, a newly added dependency may be hidden by the App’s total size.
The baseline file should be reviewed alongside the source code, but a failed job must never overwrite it automatically. A sound workflow is for the job to report the difference, the developer to explain its cause, and the reviewer to confirm that the change matches the requirements. The baseline is then updated in the same change. This distinguishes approved growth from resetting the numbers merely to make the check pass.
Investigate Common Sources of Unexpected Growth
When the main executable grows, inspect static dependencies, generic specialization, duplicate linking, and compilation conditions. When a framework grows, verify its dependency version and embedding method. When resources grow, examine source images, fonts, audio and video files, and duplicated localization content.
Also avoid four common mistakes:
- Comparing results produced by different Xcode environments.
- Mixing simulator builds with physical-device archives.
- Saving only the total size without component details or a commit identifier.
- Automatically raising thresholds or overwriting the baseline after the gate fails.
The final report should include at least the commit identifier, archive configuration, total App size, main executable size, several of the largest frameworks, the increase relative to the baseline, and the pass-or-fail result. Keep the report even when the gate passes so that slow but continuous growth remains visible. When the gate fails, retain both the archive and the detailed measurements for verification in the same environment instead of relying on developers to rebuild locally.
Frequently asked questions
Why is comparing only the final IPA size unreliable?
An IPA is compressed, so its size can change with resource content, file ordering, and archiver behavior. Use the uncompressed app bundle and main executable as primary metrics, with IPA size as a secondary signal.
What threshold should an app size regression gate use?
Measure several normal releases first, then combine an absolute allowance with a relative percentage. The example values of 8 MiB and 3% are starting points and should be tuned from project history.
Should a dependency upgrade automatically replace the baseline?
No. Inspect the added symbols, resources, or frameworks and confirm that the increase is intentional. Update the baseline only after review, with the reason recorded in the same change.
Move your next iOS build to MiniDebug M4.
M4, 16GB RAM, and a 256GB SSD are included. Rent by the day, week, month, or quarter, and choose from five locations: Singapore, Tokyo, Seoul, Hong Kong, or the US East Coast. Actual availability is shown in real time by the console.