SwiftQARandom

How would you reduce an app's pre-main launch time?

What happens before main

  1. The kernel execs the process and maps dyld and the main binary.
  2. dyld recursively finds and maps every dependent dylib and validates code signatures.
  3. It applies fixups: rebasing (adjusting pointers for ASLR) and binding (resolving pointers to symbols in other images). Both touch memory pages, which causes page faults and I/O on cold launch.
  4. Objective-C setup: registers classes and categories, uniques selectors.
  5. Initializers run: Objective-C +load, C/C++ __attribute__((constructor)), and C++ static constructors.

Measure first

Don’t optimize blind. Use the Instruments App Launch template and Xcode Organizer or MetricKit for field data. DYLD_PRINT_STATISTICS is a useful quick check, but it’s less reliable on newer OS versions, so treat Instruments as the source of truth. Two caveats:

  • Measure true cold launches (for example, after a reboot). Warm launches reuse dyld’s cached closure and are unrepresentative.
  • Since iOS 15, the system can prewarm an app, so pre-main may run before the user taps. Measure from process start to first rendered frame, not just pre-main, or the optimization targets a number users never feel — for many apps pre-main is a minority of launch time, and the bigger wins are post-main.

Levers, by phase

1. Fewer dynamic images. Every dylib costs a map, a signature check, fixups, and an initializer pass. Merge or statically link internal modules, and use mergeable libraries to keep fast debug builds while shipping one binary. Apple’s old rule of thumb was single digits for custom dylibs.

2. Cheaper fixups.

  • A smaller binary means fewer pages touched and fewer page faults — use dead-strip, remove unused code, and watch for generic or inlining bloat.
  • Fewer Objective-C classes, categories, and selectors means less runtime registration.
  • Hide symbols with -fvisibility=hidden and export only what’s needed, so there are fewer binds.
  • Chained fixups, which come with modern deployment targets, make rebase and bind much cheaper, so raising the minimum OS version helps.

3. Kill pre-main initializers.

  • +load runs synchronously for every class and category that implements it; the usual offenders are method swizzling and SDK auto-registration. Replace it with +initialize (lazy, on first use) or an explicit setup call at a point you control — see what +load does and why it’s expensive for the full mechanics.
  • C++ static initializers and constructor functions show up in __mod_init_func or __init_offsets. Turn on -Wglobal-constructors to find them, and make globals lazy (function-local statics) or constexpr.
  • Swift is mostly safe here, because globals and static let are lazy. That cost isn’t eliminated, just moved to first access — which is post-main.
  • Third-party SDKs are the usual culprits. Audit them, and defer their initialization until after the first frame.

4. Be careful with -ObjC. It forces the linker to pull in every Objective-C class and category from every static library, even if unused. That defeats dead-stripping and inflates class registration and +load count. It exists for categories in static libraries; the fix is a targeted -force_load for the specific libraries that need it, not the global flag. -all_load is worse.

5. Layout. A linker order file that clusters startup-path functions reduces page faults during launch. This pays off more post-main, but it’s cheap to add.

Keeping it from regressing

Put a launch budget in CI: an XCTApplicationLaunchMetric test plus a binary-inspection check that fails the build when +load or constructor counts, or the dylib count, go up. Otherwise someone adds an SDK and quietly undoes the work. Pair that with MetricKit or Organizer for field regression tracking and a named owner for startup.

One-liner if pressed: “Measure cold launch first, since prewarming and the post-main share matter. Then cut dylib count, eliminate +load and static constructors, scope -ObjC with -force_load, and add CI guardrails so it doesn’t regress.”