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:

  1. How do you identify whatโ€™s slow?
  2. What optimizations would you implement?
  3. 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:

  1. Reduced dynamic frameworks: 38 โ†’ 15 frameworks (-250ms pre-main)
  2. Deferred analytics: Moved to background (-180ms)
  3. Lazy database: Only init when first query (-320ms)
  4. Cached first screen data: Show stale data instantly (-400ms perceived)
  5. 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


๐Ÿ’ก 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!