
Slow app launch costs users. Learn how to measure and cut iOS app launch time with Xcode Instruments, MetricKit, and proven startup fixes.
Your users decide how they feel about your app before they see a single feature. They tap the icon, and they wait. If that wait feels long, they notice. If it happens every day, they leave.
This guide walks you through how iOS app launch actually works, how to measure it properly, and the fixes that make the biggest difference. It is written for iOS engineers and the product owners who work with them, and it comes from the practices we use every day as an iOS app development company.
Launch is the first interaction in every session. A slow start makes the whole app feel slow, even if every screen after it is fast.
Here is what a slow launch costs you:
Apple's long-standing guidance is to render your first frame within 400 milliseconds. That is the number to design toward.
Not every launch is the same. Before you optimize, know which one you are measuring.
| Launch type | What is happening | Typical speed |
|---|---|---|
| Cold launch | App is not in memory. Often after a reboot or a long time unused. The system loads everything from scratch. | Slowest |
| Warm launch | App was recently terminated. Some of it is still cached in memory. | Faster |
| Resume | App is suspended in the background and comes back to the foreground. Technically not a launch. | Fastest |
Cold launch is your worst case, so that is where optimization pays off most. Warm launch is what most users feel day to day.
One more thing to know: prewarming. Since iOS 15, the system can start your app's process ahead of time, before the user taps the icon. That means the process start time is not a reliable starting point for your measurements. Measure from the moment your code actually runs, or use Apple's own launch metrics, which account for this.
Launch has two big phases. Knowing them tells you where to look.
This is everything before your main() function or @main entry point runs. You do not write this code, but your choices shape how long it takes.
+load methods, C++ static constructors, and __attribute__((constructor)) functions run here. They run on the main thread, before anything else.This is your code.
application(_:didFinishLaunchingWithOptions:) or your SwiftUI App initializer runs.Most teams find the bulk of their fixable time in post-main. But large, older codebases often carry real weight in pre-main too.
You cannot fix what you have not measured. Use more than one source, because lab numbers and real-world numbers tell different stories.
Open Window > Organizer > Launch Time in Xcode. You get launch data from real devices of users who opted in to share analytics. Split it by device and app version. This is your source of truth for how launch feels in the wild.
Profile your app with the App Launch template in Instruments. It breaks launch into phases, shows what the main thread is doing at each moment, and points you at the slow parts. Xcode 27 adds a flame graph view to this instrument, which makes it much faster to spot the heaviest call paths.
Always profile a Release build on a real device, ideally an older one. The simulator and debug builds do not reflect what your users experience.
MetricKit delivers launch data from your users' devices straight to your app, usually once a day. You can send it to your own analytics backend.
import MetricKit
final class LaunchMetricsCollector: NSObject, MXMetricManagerSubscriber {
func start() {
MXMetricManager.shared.add(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
if let launch = payload.applicationLaunchMetrics {
// Histograms of time to first draw and resume time
let firstDraw = launch.histogrammedTimeToFirstDraw
let resume = launch.histogrammedApplicationResumeTime
// Send to your analytics backend here
print(firstDraw, resume)
}
}
}
}Add a launch test to your CI so a slow launch never ships unnoticed.
import XCTest
final class LaunchPerformanceTests: XCTestCase {
func testLaunchPerformance() {
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}Set a baseline. If a pull request pushes launch time past it, the test flags it.
Mark the steps that matter to your app, such as "config loaded" or "home feed ready." They show up in Instruments next to the system's own launch data.
import os
let launchLog = OSSignposter(subsystem: "com.yourcompany.app", category: "Launch")
let state = launchLog.beginInterval("LoadConfig")
loadConfiguration()
launchLog.endInterval("LoadConfig", state)Every dynamic framework is extra work for dyld at launch. If you have dozens, it shows.
Code that runs before main() blocks everything else.
+load with +initialize, or better, with explicit setup called when the feature is first used.Less code means less to load and fix up.
This is where most teams find their biggest wins.
didFinishLaunching leanYour app delegate is not a to-do list for everything the app will ever need. Only do what is required to show the first screen.
Before:
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
AnalyticsSDK.start()
CrashReporter.start()
AdsSDK.initialize()
RemoteConfig.fetchAndWait() // Blocks the main thread
Database.migrateIfNeeded() // Blocks the main thread
ImageCache.warmUp()
return true
}After:
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
CrashReporter.start() // Needed early to catch launch crashes
Task.detached(priority: .utility) {
AnalyticsSDK.start()
AdsSDK.initialize()
await RemoteConfig.fetch() // Use cached values for this session
}
return true
}Ask one question for every line: does the first screen need this? If not, move it.
The main thread draws your UI. Anything else on it during launch delays the first frame.
UserDefaults reads of large values and heavy Keychain access in the startup path.A schema migration on a big local database can take seconds. If you must migrate:
Use this as a starting audit for your app.
XCTApplicationLaunchMetric test to CI+load methods and heavy static initializersdidFinishLaunching to only what the first screen needsdidFinishLaunching. Check whether that is truly required.Launch time problems are often a symptom of a bigger issue: years of added SDKs, a tangled startup sequence, or an architecture that was never built with performance in mind. Most of the fixes in this guide are things your team can handle in a sprint. But if the startup path has grown tangled over years, a focused audit can save weeks of guesswork. That is the kind of work we do at Techiebutler: profile on real devices, prioritize impact, and fix alongside your team.