App Launch Time Optimization
Published:
๐ฏ Problem Statement
Scenario: Your iOS app currently takes 3 seconds to launch (from tap to first interactive screen). Leadership wants it under 1 second.
Your task:
- How do you identify whatโs slow?
- What optimizations would you implement?
- What are the trade-offs?
This is a common real-world problem at scale companies like Meta, Snap, Uber.
๐ Step 1: Measure & Identify Bottlenecks
Use Instruments - Time Profiler
# In Xcode:
1. Product โ Profile (โI)
2. Select "Time Profiler"
3. Launch app and stop after first screen appears
4. Analyze call tree
What to look for:
- Functions taking > 100ms
- Main thread blocking operations
- Synchronous file I/O
- Database initialization
- Network requests on launch
App Launch Phases
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Phase 1: Pre-main Time (dyld loading) โ
โ - Loading dynamic libraries โ
โ - Objective-C runtime initialization โ
โ - +load methods โ
โ Target: < 200ms โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Phase 2: Main() to First Frame โ
โ - AppDelegate didFinishLaunching โ
โ - Initial view controller creation โ
โ - View loading and layout โ
โ Target: < 400ms โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Phase 3: First Frame to Interactive โ
โ - Additional view setup โ
โ - Data fetching โ
โ - UI updates โ
โ Target: < 400ms โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Total Target: < 1000ms
Measure Pre-main Time
Add environment variable in Xcode scheme:
DYLD_PRINT_STATISTICS = 1
Output:
Total pre-main time: 387.45 milliseconds (100.0%)
dylib loading time: 89.23 milliseconds (23.0%)
rebase/binding time: 45.12 milliseconds (11.6%)
ObjC setup time: 156.78 milliseconds (40.5%)
initializer time: 96.32 milliseconds (24.9%)
โก Optimization Strategies
1. Reduce Pre-main Time
Problem: Too many dynamic frameworks
Solution:
// Before: 50 dynamic frameworks
// After: Merge into fewer frameworks or use static linking
// In your Podfile:
use_frameworks! :linkage => :static // Static instead of dynamic
// Reduce from 50 โ 10 frameworks
// Impact: 200ms โ 80ms pre-main time
Avoid +load methods:
// โ BAD: Runs at launch
+ (void)load {
// Heavy initialization
[self setupLogging];
}
// โ
GOOD: Run lazily
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self setupLogging];
});
}
2. Defer Non-Critical Initialization
Before (slow):
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ALL happening on main thread, blocking launch
setupAnalytics() // 200ms
initializeDatabase() // 300ms
setupCrashReporting() // 100ms
loadUserPreferences() // 150ms
setupNotifications() // 100ms
// Total: 850ms ๐ฑ
return true
}
After (fast):
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Only CRITICAL initialization on main thread
setupWindow() // 50ms - REQUIRED
// Everything else: async or lazy
DispatchQueue.global(qos: .userInitiated).async {
self.setupAnalytics() // Background
self.setupCrashReporting() // Background
}
// Lazy - only when needed
// initializeDatabase() - called on first DB access
// loadUserPreferences() - called when user settings accessed
return true
}
Impact: 850ms โ 50ms in didFinishLaunching
3. Lazy Initialization Pattern
class AppServices {
// Lazy properties only initialize when first accessed
lazy var database: DatabaseManager = {
return DatabaseManager() // Heavy initialization
}()
lazy var analytics: AnalyticsManager = {
let manager = AnalyticsManager()
manager.configure()
return manager
}()
lazy var networking: NetworkManager = {
return NetworkManager(baseURL: Config.apiURL)
}()
}
// Usage:
// Database only initializes if/when actually used
let user = AppServices.shared.database.fetchUser()
4. Parallelize Independent Work
func setupApp() {
let group = DispatchGroup()
// Run multiple things in parallel
group.enter()
DispatchQueue.global().async {
self.setupAnalytics()
group.leave()
}
group.enter()
DispatchQueue.global().async {
self.loadRemoteConfig()
group.leave()
}
group.enter()
DispatchQueue.global().async {
self.warmUpImageCache()
group.leave()
}
// Wait for critical tasks only
group.notify(queue: .main) {
print("Critical async setup complete")
}
}
5. Optimize First View Controller
class FeedViewController: UIViewController {
// โ BAD: Everything in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
setupTableView() // 100ms
loadUserProfile() // 200ms - NETWORK CALL!
setupNotifications() // 50ms
fetchFeed() // 300ms - NETWORK CALL!
setupAnalytics() // 100ms
// Total: 750ms blocked
}
// โ
GOOD: Only UI in viewDidLoad, rest async
override func viewDidLoad() {
super.viewDidLoad()
setupTableView() // 100ms - REQUIRED
// Everything else: async
Task {
await loadUserProfile()
await fetchFeed()
}
DispatchQueue.global().async {
self.setupNotifications()
self.setupAnalytics()
}
}
}
๐ Real-World Example: Snapchat Memories
Before Optimization:
- Launch time: 2.8 seconds
- User complained about slowness
Measured with Instruments:
- Pre-main: 450ms (16%)
- didFinishLaunching: 1200ms (43%)
- First VC load: 1150ms (41%)
Optimizations Applied:
- Reduced dynamic frameworks: 38 โ 15 frameworks (-250ms pre-main)
- Deferred analytics: Moved to background (-180ms)
- Lazy database: Only init when first query (-320ms)
- Cached first screen data: Show stale data instantly (-400ms perceived)
- Async image decoding: Moved off main thread (-200ms)
After Optimization:
- Launch time: 0.9 seconds (68% improvement!)
- Pre-main: 200ms
- didFinishLaunching: 150ms
- First VC: 550ms
๐ก Interview Tips
What Good Answers Include:
โ Measurement first: โIโd use Instruments Time Profiler to identify bottlenecksโ
โ Specific numbers: โReduced from 450ms to 200ms byโฆโ
โ Trade-offs: โShowing stale data is faster but might confuse users if outdatedโ
โ Real tools: Name actual tools (Instruments, DYLD_PRINT_STATISTICS)
โ Prioritization: โFirst VC needs to be < 400ms, everything else can be lazyโ
Red Flags to Avoid:
โ โJust make everything fasterโ (not specific)
โ โCache everythingโ (without discussing memory limits)
โ โUse third-party library Xโ (without understanding what it does)
โ โThis isnโt a problem on modern devicesโ (scale matters)
๐ฏ Summary Checklist
- Use Instruments Time Profiler to measure
- Check DYLD_PRINT_STATISTICS for pre-main time
- Reduce dynamic frameworks (merge or static link)
- Defer non-critical work to background or lazy load
- Parallelize independent initialization tasks
- Optimize first VC - minimal work in viewDidLoad
- Async everything possible
- Cache first screen data for instant display
- Measure again to verify improvements
- Set up monitoring to prevent regressions
๐ Further Reading
- WWDC: Optimizing App Startup Time
- WWDC: Improving Battery Life and Performance
- Apple Docs: Reducing Your Appโs Launch Time
๐ก Meta/Snap Interview Note: At scale companies, every 100ms of launch time affects millions of users daily. They care deeply about this. Come prepared with specific techniques and real measurements!
Share on
Twitter Facebook LinkedInโ Buy me a coffee! ๐
If you found this article helpful, consider buying me a coffee to support my work! ๐