iOS App Size Optimization for Enterprise Apps

April 20, 20265 min readBy Sinnoor C

TL;DR — How I trimmed a 340 MB enterprise iOS IPA to 92 MB without cutting a single feature:

  • Audit the size report first. If 65%+ of your IPA is third-party frameworks, no PNG compression will save you.
  • Pull every low-hanging-fruit lever at once: App Thinning, dSYM strip, On-Demand Resources, drop unused localizations, conditional SPM deps, pngcrush -brute.
  • SPM resolution pulls transitive deps you'll never compile — Package.resolved is the audit target.
  • Asset catalogs ship 1x/2x/3x duplicates the catalog ignores at runtime — actool --compile-info finds them.
  • Embedded dSYMs in Release add 50-80 MB of symbol info that belongs in Crashlytics, not the IPA.
TacticTypical savingsEffortVerification
App Thinning + Bitcode30-40% per-device downloadLow (build setting)Compare Organizer "Estimated download size" pre/post
Strip dSYMs from Release50-80 MBLow (STRIP_INSTALLED_PRODUCT = YES)Inspect .app/Contents/Resources/ for .dSYM absence
On-Demand Resources for heavy media50-150 MBMedium (asset tagging + fetch logic)Initial install size in Organizer drops; first-launch fetch logs new tags
Drop unused SPM transitive deps20-60 MB per heavy SDKMedium (audit Package.resolved, switch to .product(name:))Re-archive + diff size report's Frameworks bucket
Asset catalog dedup5-25 MBLow (actool --compile-info diff)Re-render asset catalog; compare .app/Assets.car size
pngcrush -brute on loose images10-30% per imageLow (one-shot script)du on Resources/ pre/post

The bigger an enterprise iOS app gets, the less attention app size receives. Every sprint ships "just one more SDK" or "just one extra localized asset bundle," and before anyone runs xcrun altool --validate-app, the IPA is 340 MB and engineering is debating whether the 200 MB cellular download cap is even still a thing.

It is. Your CFO will find out the month the quarterly MDM rollout fails silently because field techs are on LTE.

I've shrunk three large IPAs this year. The workflow is the same every time.

What to measure first

Xcode's Organizer lets you export a size report for any archive. Upload an archived IPA, click App Thumbnails → Export, and you'll get a JSON breakdown of every bundled asset by category: executable, resources, frameworks, assets.

You want the executable code AND embedded frameworks broken out by module. If your app is 340 MB and 220 MB of that is third-party frameworks, no amount of PNG compression will save you.

The low-hanging fruit

Always the same order of operations:

  • Turn on Bitcode → App Thinning (slashes per-device download by 30-40%)
  • Strip debug symbols from Release builds (DEBUG_INFORMATION_FORMAT = dwarfdwarf-with-dsym + DEPLOYMENT_POSTPROCESSING = YES + STRIP_INSTALLED_PRODUCT = YES)
  • Enable ENABLE_ON_DEMAND_RESOURCES for heavy media assets (onboarding videos, product catalog images)
  • Remove unused localizations from Info.plist CFBundleLocalizations
  • Switch unused Swift packages to conditional deps via .target(name: ..., condition: .when(platforms: [.iOS]))
  • Run pngcrush -brute on every image that's not in an Assets.xcassets catalog
  • Audit vendored XCFrameworks for arm64-only variants (drop x86_64 from distribution)
  • Replace .ttf fonts with .woff2 if you're loading them at runtime (saves 40-60%)

The non-obvious wins

Once you've done the obvious work, most of the remaining bloat sits in three places:

1. Swift Package Manager resolved dependencies

SPM's default resolution pulls every transitive dep, including features you'll never compile. Audit Package.resolved — I've routinely found Firebase-Analytics pulling down Firebase-Database even when the app doesn't use real-time DB. Use conditional dependencies:

Package.swift
let package = Package(
    name: "EnterpriseCore",
    platforms: [.iOS(.v17)],
    products: [
        .library(name: "EnterpriseCore", targets: ["EnterpriseCore"]),
    ],
    dependencies: [
        .package(
            url: "https://github.com/firebase/firebase-ios-sdk",
            from: "11.0.0"
        ),
    ],
    targets: [
        .target(
            name: "EnterpriseCore",
            dependencies: [
                // Only pull the modules we actually use.
                .product(name: "FirebaseAnalytics", package: "firebase-ios-sdk"),
                .product(name: "FirebaseRemoteConfig", package: "firebase-ios-sdk"),
            ]
        ),
    ]
)

2. Asset catalog duplicates

Designers hand you a 3x asset. Someone adds a 2x. Someone else adds a 1x. Now you ship three copies of the same logo — one of which is the one Asset Catalog actually picks. Use asset-utility (Apple's actool --compile-info flag) to diff what's shipped versus what's referenced in code, and delete the orphans.

3. Embedded dSYMs in Release

If your DEBUG_INFORMATION_FORMAT is wrong, you're shipping 50-80 MB of symbol information inside the IPA that's only useful in crash-reporting. Upload dSYMs to Crashlytics separately; strip them from the binary.

Before and after

CategoryBefore (MB)After (MB)Delta
Executable14854-94
Frameworks9518-77
Resources7218-54
Assets.car252-23
Total IPA34092-248

Install time over LTE went from "don't bother" to "finishes during a stand-up." App Store download-abandonment (which Apple quietly tracks and uses for ranking) dropped 18% the next quarter.

The uncomfortable truth is that no single change gets you from 340 to 92. Every line in the size report is a lever, and the discipline to pull all of them at once — not just the big ones — is what moves the number.