Greetings, traveler!
There is a moment every iOS engineer has experienced but rarely examined in detail. You tap an app icon, and within a fraction of a second the interface appears, already interactive, already alive. That path from tap to first frame looks simple on the surface, yet it crosses several layers of the system, from low-level process creation to UIKit building the first view hierarchy. Once you start looking closely, the sequence becomes much more interesting, especially from a performance perspective.
This article walks through that sequence step by step, focusing on what actually happens before the first screen appears and where launch time is really spent.
Cold start, warm start, and resume
Before diving into internals, it helps to distinguish three different entry scenarios.
A cold start happens when the application process does not exist. The system has to create a new process, load the executable, initialize runtime components, and then enter the app lifecycle. This is the slowest path and the one most sensitive to architectural decisions.
A warm start also creates a new process, but under more favorable conditions. The app may have been launched recently, system caches are populated, and parts of the runtime environment are already prepared. The sequence of steps remains the same, though the cost tends to be lower and more consistent.
A resume does not create a process at all. The application is already in memory, typically in the background. The system brings it back to the foreground and continues execution. Many lifecycle callbacks differ in this case, and none of the launch path discussed below is executed again.
Process creation and system work
After the user taps the icon, iOS decides whether to resume an existing process or start a new one. In a cold or warm start, the system creates a process for the application and begins preparing it for execution.
At this stage, none of your code has run yet. The system is responsible for:
- locating the application’s executable on disk
- creating the process and its address space
- preparing low-level runtime components
- delegating loading responsibilities to the dynamic linker
A significant portion of launch time is spent here, before any Swift or UIKit code executes. This is why an empty AppDelegate does not guarantee a fast launch.
dyld and loading the binary
The next step is handled by dyld, the dynamic linker. It loads the application’s Mach-O executable and all required dynamic libraries into memory.
This involves several operations:
- mapping binary segments into the process address space
- resolving symbol references across libraries
- applying address fixups due to address space layout randomization
- preparing the runtime so execution can begin
The amount of work dyld performs depends heavily on the number of dynamic libraries and frameworks linked into the app. Each additional dependency adds overhead during launch. This is one of the reasons large dependency graphs can affect startup time even before application code runs.
At this point, the process is structurally ready to execute. Control can move forward to the program entry point.
Pre-main time
Everything described so far falls into what is commonly called pre-main time. This phase ends when execution reaches the main function.
Pre-main includes:
- dyld loading and linking
- initialization of system libraries
- Objective-C runtime setup
- Swift runtime initialization
- execution of static initializers
Static initializers deserve special attention. Any work performed in global variables, static properties, or Objective-C +load methods happens here. That work is invisible from the perspective of AppDelegate, yet it directly impacts launch performance.
If something feels slow and didFinishLaunching looks clean, the issue often sits in pre-main.
main and UIApplicationMain
Once pre-main work completes, execution enters main. In modern Swift apps, main is rarely written explicitly, but it still exists.
Its primary role is to call UIApplicationMain. That function transfers control to UIKit and starts the application lifecycle.
A simplified version looks like this:
UIApplicationMain(
CommandLine.argc,
CommandLine.unsafeArgv,
nil,
NSStringFromClass(AppDelegate.self)
)From this point forward, the system operates within UIKit abstractions.
Creating UIApplication and AppDelegate
UIApplicationMain creates the central objects of the application:
- a singleton instance of
UIApplication - the application delegate (
AppDelegate)
UIApplication manages the event loop, application state, and communication with the system. The delegate receives lifecycle callbacks and provides configuration.
The first methods called on the delegate are:
application(_:willFinishLaunchingWithOptions:)
application(_:didFinishLaunchingWithOptions:)These methods form the earliest point where application-specific code participates in launch.
Scene setup and window creation
Modern iOS applications typically use the scene-based lifecycle. In this model, UI setup is delegated to UIScene rather than handled entirely in AppDelegate.
After didFinishLaunching, UIKit creates or connects a scene. The key entry point becomes:
scene(_:willConnectTo:options:)This is where the application usually creates its window and root view controller:
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = HomeViewController()
window.makeKeyAndVisible()
self.window = window
}
}If the app relies on storyboards, UIKit may construct the initial interface automatically based on configuration. In programmatic setups, this responsibility lies entirely with the developer.
In older, non-scene applications, the same work happens inside AppDelegate.
Building the first screen
Once the root view controller is assigned, UIKit begins constructing the view hierarchy.
This includes:
- initializing views and controllers
- applying layout constraints
- performing layout passes
- rendering the first frame
The moment the first frame is committed to the screen marks the end of the launch sequence from the user’s perspective. Everything before that contributes to perceived launch time.
The launch screen remains visible until this first frame is ready. That transition often hides the complexity of what just happened.
where time is spent
Launch time is usually divided into two broad segments:
- pre-main time
- post-main time
Pre-main is dominated by dyld, runtime initialization, and static initializers.
Post-main includes:
UIApplicationMainsetup- delegate callbacks
- scene creation
- UI construction and rendering
Optimizing launch requires understanding which part dominates in a given application. Guessing rarely works.
Optimizing launch time
A practical approach to launch optimization follows three ideas: reduce work, delay work, and measure correctly.
Reducing work often starts with dependencies. Fewer dynamic frameworks reduce dyld overhead. Removing unnecessary libraries has a direct effect on pre-main.
Static initialization should remain minimal. Heavy work in global scope or static properties executes before main and is hard to control once it grows.
Delaying work focuses on what happens after UIApplicationMain. Only the code required to display the first screen should run on the critical path. Everything else can move to background tasks or later lifecycle stages.
A simple example:
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
analytics.startLightweight()
return true
}
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
setupWindow(scene)
Task.detached(priority: .utility) {
await preloadResources()
}
}The first screen appears quickly, while non-essential work continues asynchronously.
Measuring launch time requires discipline. Release builds provide realistic numbers. Warm launches produce more stable measurements. Older devices reveal performance issues earlier than newer ones.
Putting it all together
From the outside, launching an app looks instantaneous. Under the hood, it moves through a precise sequence:
- the system creates a process
- dyld loads the executable and dependencies
- runtime components initialize during pre-main
- execution reaches
main, which callsUIApplicationMain - UIKit creates
UIApplicationand the delegate - lifecycle callbacks configure the application
- a scene connects and creates a window
- the root view controller defines the initial UI
- UIKit renders the first frame
Each of these steps carries a cost. Some of them are easy to control, others require architectural decisions.
Once you start looking at launch this way, performance issues become easier to reason about. Slow startup rarely comes from a single mistake. It is usually the accumulation of small decisions across dependencies, initialization patterns, and UI setup.
That is also why launch time is such a good proxy for overall application health. It exposes both low-level inefficiencies and high-level design trade-offs in a way few other metrics can.
This style of reasoning aligns well with practical writing guidance: avoid inflated claims, prefer concrete mechanisms, and keep explanations grounded in observable behavior
