Launch Phases
iOS App Launch Sequence
When a user taps your app icon, iOS executes a series of steps before your code runs:
- pre-main phase: dyld loads the dynamic libraries, rebase and bind symbols, run initializers
- main(): Your main function executes
- UIKit setup: UIApplication and AppDelegate or App lifecycle
- First frame: Your initial view appears on screen
pre-main Phase
Before main() is called, dyld (dynamic linker) performs:
- Loading Mach-O binary and all linked frameworks
- Rebasing: adjusting pointers for ASLR (Address Space Layout Randomization)
- Binding: resolving external symbols
- Running static constructors and load methods
This phase is affected by the number and size of frameworks you link.
main() to First Frame
After main(), your app must:
- Initialize AppDelegate or SwiftUI App
- Set up the window and root view controller
- Load initial data
- Render the first frame
The time from app launch to first frame should be under 400ms for a good user experience. Apple recommends under 200ms.
Impact on Users
Slow startup is one of the top reasons users abandon apps. A 1-second delay in launch time can reduce user engagement by 20%. The system also terminates apps that take too long to launch, showing the user a blank screen.
Measuring Startup Time
Using Instruments App Launch Template
The most accurate way to measure launch time is with Instruments. Select the App Launch template which shows:
- Time to first frame
- Pre-main time breakdown
- dyld loading time
- Runtime initialization time
Using DTrace for Quick Measurement
From the terminal, measure time from process start to main:
dtrace -n 'proc:::exec-success /execname == "MyApp"/ { self->start = timestamp; } \
pid$target::main:entry /self->start/ { printf("Time to main: %d ms", (timestamp - self->start) / 1000000); exit(0); }' \
-c /path/to/MyApp.app/MyApp
Adding Custom Timestamps
Log timestamps at key initialization points:
@main
struct MyApp: App {
init() {
let launchStart = CFAbsoluteTimeGetCurrent()
// Heavy initialization
setupDependencies()
setupAnalytics()
let launchEnd = CFAbsoluteTimeGetCurrent()
print("Launch init: \(launchEnd - launchStart)ms")
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
Testing on Older Devices
Always test on the oldest device your app supports. Launch time on an iPhone SE is significantly different from an iPhone 15 Pro. Test with:
- Cold launch (device restarted)
- Warm launch (app was in background)
- Release builds only (Debug builds are much slower)
Optimization Strategies
Lazy Initialization
Defer expensive work until it is actually needed:
// Bad: Initialize everything at launch
class AppContainer {
let analytics = AnalyticsEngine()
let network = NetworkService()
let database = DatabaseManager()
let imageCache = ImageCache()
}
// Good: Lazy initialization
class AppContainer {
lazy var analytics = AnalyticsEngine()
lazy var network = NetworkService()
lazy var database = DatabaseManager()
lazy var imageCache = ImageCache()
}
Deferred Non-Essential Setup
Move non-critical initialization after the first frame:
func applicationDidBecomeActive(_ application: UIApplication) {
// First frame has already rendered
DispatchQueue.main.async {
self.setupCrashReporting()
self.setupAnalytics()
self.checkForUpdates()
}
}
Reduce Framework Count
Each linked framework adds to pre-main time. Audit your frameworks:
- Remove unused framework dependencies
- Use weak linking for optional frameworks
- Consider modularizing your app to load features on demand
Optimize Dynamic Libraries
- Prefer static libraries over dynamic when possible
- Merge small frameworks into a single module
- Use -ObjC linker flag only when necessary
Binary Size Optimization
Smaller binaries load faster:
- Enable Link-Time Optimization (LTO)
- Strip unused symbols
- Use Asset Catalogs for images instead of bundle resources
- Enable App Thinning in your scheme settings
Deferred Storyboard Loading
If using storyboards, mark initial view controllers as not immediate and load them manually:
// In Info.plist, set UIApplicationSceneManifest
// Use SwiftUI App lifecycle instead of storyboard for fastest launch
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
SwiftUI App lifecycle is faster than UIKit storyboard loading because it avoids storyboard parsing overhead.
Quiz
1. What happens during the pre-main phase of app launch?
2. What is Apple recommended maximum time to first frame?
3. How does lazy initialization help startup time?
4. Which app lifecycle is fastest for launch time?
Flashcards
Question
What are the three phases of iOS app launch?
Click to reveal answer
Answer
pre-main (dyld loading), main() execution, and UIKit setup to first frame.
Question
What reduces pre-main launch time?
Click to reveal answer
Answer
Reducing the number of linked frameworks, using static libraries, and minimizing load methods.
Question
What is the benefit of lazy var over let for initialization?
Click to reveal answer
Answer
lazy var defers initialization until first access, while let initializes immediately at object creation time.
Revision Notes
Key Takeaways
- 1. The pre-main phase loads all linked frameworks and is affected by framework count
- 2. Target under 400ms from launch to first frame
- 3. Lazy initialization defers expensive work until needed
- 4. Move non-critical setup to after the first frame renders
- 5. Always test launch time on the oldest supported device
Interview Tips
- • Explain the pre-main phase and what affects its duration
- • Describe techniques for reducing launch time
- • Discuss how lazy initialization improves startup
- • Walk through measuring launch time with Instruments
Cheat Sheet
Startup Optimization Quick Reference
- Pre-main: dyld loads frameworks (reduce linked frameworks)
- main() to first frame: target under 400ms
- Use lazy initialization for non-essential objects
- Defer analytics and crash reporting after first frame
- Prefer SwiftUI App lifecycle over storyboards
- Test on oldest supported device with cold launch