Where does binary size come from, how do you measure and shrink it, and what do optimisation level, stripping and linking change?

21.7.2 · L5 · Interview

Predict first

Build the same generic Swift program unoptimised, optimised, and optimised then stripped. Is the unoptimised build bigger than the optimised one, and is the stripped one the smallest?

// === gen.sh ===
for i in $(seq 1 30); do
cat <<EOS
struct S$i<T: Numeric> { var v: T; func f(_ x: Int) -> Int { (0..<x).map { \$0 * $i }.filter { \$0 % 3 == 0 }.reduce(0, +) }; func g(_ a: [T]) -> [T] { a.map { \$0 * v } } }
EOS
done
echo 'var t = 0'
for i in $(seq 1 30); do echo "t += S$i(v: $i).f(CommandLine.arguments.count + $i) + S$i(v: 2.5).g([1.0]).count"; done
echo 'print(t)'
// === Package.swift ===
// swift-tools-version:5.9
import PackageDescription
let package = Package(name: "Z")
  • Yes to both
  • No, optimising makes a bigger binary
  • The stripped one is the largest

Mental model. Packing a suitcase: the heavy things are shoes and coats (resources and SDKs), so you do not save much by folding socks smaller (micro-tuning code).

Measure before you cut. The App Thinning Size Report gives the per-device download and install sizes, a link map (enable Write Link Map File) lists which object files and symbols take space, and tools such as size and nm break a Mach-O file into segments. Usually the largest contributors are resources (images, video, audio, fonts, bundled databases, localisations), then third-party SDKs (each framework brings its own code, often duplicated if linked statically into several targets, Q367), and then your own code, where heavy generic specialisation, big switch tables, generated code and unused features add up.

Shrink in the order of benefit. Compress and slice resources (asset catalogs, HEIC or modern formats, remove unused assets, trim languages you do not ship). Remove or replace SDKs that cost more than they give. Keep Release settings sound: dead-code stripping and a symbol strip of the shipped binary remove what is not needed at runtime (debug symbols go to a separate dSYM for crash symbolication, so keep that), and the check below shows a stripped build far smaller than the unstripped one while an unoptimised build is larger than an optimised one. The -Osize optimisation level asks the compiler to favour size, but it is not guaranteed to produce a smaller binary for every program (in the check below it did not), so measure your own app. Use -O or -Osize with whole-module optimisation, avoid unnecessary generics across module boundaries, and watch for accidental duplication. Put a size budget in CI so growth is visible release by release.

Unoptimised code and debug information take space; optimisation and stripping remove them. Whether -Osize beats -O is program specific.

Common traps

  • Assuming -Osize always shrinks the app. It is a heuristic; compare builds of your real app.
  • Shipping with symbols and debug information in the binary. Strip the product and keep the dSYM separately.

They might follow up with

  • Where do you look first? At resources and third-party SDKs, because they usually outweigh your own code.
  • How does static linking affect size? Linking the same static library into several targets duplicates its code in each; share a dynamic framework or consolidate (Q367).

Related questions

  • 21.7.1 What is app thinning, how do on-demand resources and Background Assets reduce install size, and what happened…
  • 21.3.2 How do you build a performance culture on a team: budgets, regression gates, ownership and release guardrails?
  • 16.3.1 How do static and dynamic linking differ for an app, what do they cost at launch and in size, and what are…

← What is app thinning, how do on-demand resources and Background…