The iOS Launch Performance Agent Skill


Greetings, traveler!

AI agent skills are reusable instructions that teach an agent how to handle a specific kind of task. They can define the focus, workflow, constraints, and references the agent should use instead of starting from scratch every time.

This article is part of a series about building practical skills for iOS development. The first article explains what skills are, how they differ from files like AGENTS.md and CLAUDE.md, and how they can be used in real engineering workflows.

You can read that overview here: [link to the skills article]

This article focuses on a more specific case: an AI agent skill for iOS launch performance.

What this iOS launch performance skill focuses on

The ios-launch-performance skill is built for one narrow job: helping an agent reason about app startup. It should not be used as a generic iOS performance checklist. The scope is launch-specific code, from the moment the app process starts to the first visible UI and early responsiveness.

A slow app launch can come from many places: dyld work, static initialization, UIApplicationDelegate, UISceneDelegate, SwiftUI App setup, dependency container construction, SDK initialization, root view creation, or work that starts right after the first frame but still makes the app feel frozen.

The skill treats launch as a pipeline. Before suggesting fixes, the agent has to classify the scenario and the expensive phase. Is this a cold launch, warm launch, prewarmed launch, first install launch, update launch, or just a resume from background? These are different cases, and mixing them into one number usually leads to bad conclusions.

First, locate the launch path. Then classify the slow phase. After that, split startup work by necessity: what must happen before the first frame, what must happen before the first interaction, what can wait until after authentication, and what belongs to a later feature. This prevents the agent from giving generic advice like “move it off the main thread” without proving that the work is both safe to move and relevant to launch.

A large part of the skill is about hidden eager work. It tells the agent to look for +load, constructor functions, Swift globals, static properties, eager singletons, dependency graph construction, SDK auto-registration, synchronous I/O, database work, keychain access, blocking locks, and other costs that can quietly move onto the launch path.

The skill also pushes against common launch-performance myths. It does not blame dyld by default. It does not recommend converting every module to static linking. It does not treat framework count as a magic number. It does not assume that async work is harmless just because it is scheduled after startup. Each recommendation needs evidence and a validation path.

The expected output is also structured: launch classification, suspected phase, critical path, findings, recommended changes, and validation. That makes the agent’s response easier to review. More importantly, it keeps the discussion tied to measurable startup behavior instead of turning it into a broad architecture opinion.

AppDelegate, SceneDelegate, and first frame

The first reference focuses on the part of launch that starts after the app enters its own lifecycle code. In UIKit apps, this usually means UIApplicationDelegate, UISceneDelegate, window creation, root view controller setup, first-screen routing, and the work that happens before the first app-rendered frame.

This is where launch problems often become very concrete. The app may build a full dependency graph in didFinishLaunching, open a database before the first screen needs it, parse large files, touch the keychain too broadly, create every tab up front, or complete deep-link navigation before any valid root UI is visible.

The reference draws an important line between process-level bootstrap and scene-specific UI. App-wide services should not accidentally start once per scene. Scene connection should not behave like a full application launch. This matters more in multi-window apps, where scene(_:willConnectTo:options:) can run more than once.

A useful part of this reference is its startup task classification. Some work is required before the first frame. Some is required before the first interaction. Some can run soon after launch. Some belongs to a later feature. And some is just background maintenance. That classification keeps the agent from suggesting vague deferral. If something moves later, the agent has to say what triggers it, what UI state exists before it completes, and how failure is handled.

The reference also avoids a common trap: chasing the first-frame metric at the cost of correctness. A fast empty shell is not enough if the app must first decide whether to show a login screen, a privacy lock, onboarding, or a safe main shell. The first UI should be minimal, but still correct for the current user state.

For code review, this file gives the agent practical search targets: lifecycle methods, bootstrap calls, dependency setup, blocking operations, root UI creation, and Swift concurrency work that may still run on the launch path. The goal is to connect each finding to a real lifecycle phase, not to produce a generic list of iOS performance advice.

Launch orchestration and dependency graph

This reference is for apps where launch has grown into a startup system. A few setup calls become a sequence of services, stores, SDKs, routers, feature gates, session checks, and first-screen dependencies. At some point, the real problem is no longer one slow method. The problem is that nobody can clearly explain what must run first, what can run later, and what is only there because it has always been there.

The file pushes the agent to turn ordered startup code into a graph of named steps. Each step should have a reason to exist on the launch path. It should declare what it prepares, what it depends on, whether it blocks the first frame or first interaction, whether it touches the main thread, and what happens if it fails.

This matters because launch optimization is often unsafe when dependencies are only hidden in call order or comments like “do not reorder”. Running startup work in parallel can look like an easy win, but it can also expose races, deadlocks, missed registrations, broken routing, or startup crashes that only happen on slower devices.

The reference separates the first-frame critical path from the first-interaction path, secondary work, lazy work, and maintenance. That distinction is useful. If a step is needed only by a later feature, it should not force the whole app to wait during global launch. If a step is needed before the first tap, it may not need to block the first rendered frame, as long as the UI has an honest loading, locked, disabled, or placeholder state.

Another strong part of this reference is the focus on the longest dependency chain. The slowest individual step is not always the launch bottleneck. A chain of medium-sized steps can define the minimum possible launch time if each step waits for the previous one. The agent should optimize that chain, weaken unnecessary dependencies, or reduce what the first screen needs from it.

The file also treats failure, cancellation, and timeout as part of launch design. Startup cannot assume that storage, network, configuration, or services always respond quickly. Some failures can degrade to cached state. Some should block only authenticated areas. Some safety-sensitive paths need explicit fail-open or fail-closed behavior.

For an AI agent, this reference is especially useful because it prevents reckless advice. It does not say “parallelize startup”. It says: name the steps, expose the dependencies, classify readiness, bound the parallelism, and validate both timing and correctness.

Launch taxonomy and targets

This reference exists because “launch time” is often used too loosely. One team may talk about cold launch. Another may measure warm XCTest iterations. A product dashboard may show a custom “home loaded” milestone. Someone else may be looking at resume from background. These numbers should not be compared as if they describe the same thing.

The file makes the agent classify the launch scenario first. Is this a cold launch, warm launch, prewarmed launch, first install launch, post-update launch, resume, background launch, or an unknown case? That classification changes the investigation. A migration-heavy first run is not the same as a normal returning-user launch. A resume from background is not the same as starting a new process.

It also asks the agent to identify the launch entry point. App icon launch, notification tap, universal link, widget, Live Activity, shortcut, file open, and background event can all require different routing and readiness work. A deep link may need authentication and navigation decisions that the normal app-icon path does not need.

Another useful distinction is the measurement boundary. The reference separates app icon tap to first app-rendered frame, process start to first frame, main to first screen, first meaningful content, extended launch, first interaction responsiveness, and custom business milestones. Without that boundary, a launch number is only a symptom.

The 400 ms target is treated carefully. It is an aggressive target for the first visible app frame, not a watchdog threshold and not a pre-main-only budget. The first frame can be minimal or placeholder-based, but it still has to be safe and correct. Showing sensitive cached UI before auth, lock, privacy, or account state is known is not a good launch optimization.

This reference is also useful for reviewing reports and dashboards. It pushes the agent to check build type, physical device versus simulator, device class, iOS version, data state, app version, run count, variance, percentiles, Low Power Mode, thermal state, and whether the measurement came from Instruments, XCTest, MetricKit, Organizer, custom logs, or manual timing.

The main idea is simple: do not optimize an unclear number. Classify the scenario, entry point, and boundary first. Then compare like with like.

Linking strategy

This reference is for cases where launch cost may come from the way the app is linked and packaged. It covers static libraries, dynamic frameworks, mergeable libraries, embedded framework count, app extension dependencies, vendored SDKs, optional feature dependencies, and the structure of the final release bundle.

The most useful idea here is that linking strategy changes where the cost is paid. Static linking can reduce the number of separate dynamic images loaded at launch, but it can also increase the main binary size and introduce resource, symbol, extension, or build-time tradeoffs. Dynamic frameworks can add launch work, but they may be required for vendor SDKs, app extensions, distribution boundaries, runtime loading, or development workflow. Mergeable libraries can be a good option when a project wants dynamic-library ergonomics during development but fewer dynamic images in the shipped app.

The reference also warns against a common mistake: treating declared, linked, embedded, and launch-loaded dependencies as the same thing. A dependency in Package.swift is not proof that it affects launch. A framework inside the app bundle is not automatically the bottleneck. A dependency may be present in the app but loaded only when a later feature opens.

That is why the agent should inspect the shipped product when possible. Project manifests are useful, but the final .app bundle tells a more reliable story: which frameworks are embedded, which dynamic dependencies exist, whether debug-only frameworks slipped into Release, and whether optional feature dependencies are pulled into the main launch graph.

The reference does not say “convert everything to static”. It asks the agent to classify dependencies by ownership, packaging, linkage, bundle status, load status, and launch relevance. Apple system frameworks should be separated from first-party and third-party embedded frameworks. Vendored SDKs need extra care because their linkage model may be constrained by the vendor.

Optional feature dependencies are another important part. A payment SDK used only during checkout, a map SDK used only on a map screen, or a scanning SDK used only in one flow should not automatically sit on the main launch-loaded path. Moving such dependencies later can help, but only if the feature owns its loading, fallback, and failure behavior.

For an AI agent, this reference adds useful discipline. It prevents framework-count superstition and forces release-like validation. A linkage change is only worth recommending when evidence points to binary structure and when resource bundles, app extensions, duplicate symbols, Objective-C categories, vendor constraints, symbolication, and build behavior are all accounted for.

Metrics, Instruments, XCTest, MetricKit, and production monitoring

This reference covers the measurement side of launch performance. It is used when the agent needs to interpret traces, XCTest launch metrics, MetricKit payloads, Xcode Organizer data, signposts, CI baselines, or production launch dashboards.

The core rule is simple: do not optimize launch from a single unexplained number. A number without scenario, entry point, boundary, build type, device, run count, variance, and data state can describe a symptom, but it cannot explain the cause.

The file separates several kinds of evidence. Instruments and Time Profiler are useful for local root-cause work. XCTest launch metrics are better for repeatable regression checks. MetricKit and Xcode Organizer help understand production impact and release trends. Custom telemetry can explain app-specific readiness, but only when its boundaries are clearly labeled. Manual timing and screen recordings are useful for describing the visible problem, not for proving CPU cost or root cause.

This reference also keeps measurement boundaries honest. A custom homeLoaded event is not the same as first app-rendered frame. viewDidAppear is not proof that the first frame was displayed. App-level timestamps may miss pre-main work, system-side work, or prewarming. If a metric says “launch time”, the agent should ask what interval it actually measures.

The XCTest part is practical. XCTApplicationLaunchMetric() can protect a known launch path from regressions. XCTApplicationLaunchMetric(waitUntilResponsive: true) is useful when the question includes early responsiveness, not just first draw. But XCTest still does not explain why a regression happened. For that, the agent needs traces, signposts, or focused code review.

The production side has a different role. MetricKit and Organizer can show that launch got worse for real users, across app versions, devices, OS versions, and percentiles. They help prioritize the problem and validate trends. They do not usually give enough detail to choose a safe code change by themselves.

For an AI agent, this reference is important because it separates proof from guesswork. It asks: what evidence was provided, what can it prove, what can it not prove, and what is the smallest next measurement needed? That keeps launch work grounded in comparable data instead of local anecdotes or dashboard noise.

Pre-main, dyld, and static initializers

This reference covers the part of launch that happens before the app reaches its own lifecycle code. In UIKit apps, that usually means before willFinishLaunching, didFinishLaunching, or scene connection. In SwiftUI apps, it means before the @main App type, App.init, scene construction, and root view setup.

This distinction matters because pre-main work happens before the app can apply its own startup policy. The app cannot show a placeholder, defer work after the first frame, or recover gracefully from failure until this phase is done. If expensive work hides here, moving code out of AppDelegate or .task will not fix the real problem.

The reference focuses on load-time behavior: dyld image loading, fixups, runtime registration, Objective-C +load, Objective-C +initialize, C and C++ constructors, binary initializer sections like __mod_init_func, Objective-C categories with behavior, and Swift globals or statics when they are touched early.

A useful part of this file is its precision around what should not be overclaimed. Not every Swift static let is pre-main work. Not every Objective-C category is expensive. Not every dynamic framework is the bottleneck. The risk is not the declaration itself, but expensive execution before the app reaches a controllable launch phase.

+load gets special attention because it can run when the Objective-C runtime loads a class or category. That makes it a common source of hidden startup cost. Work like networking, disk I/O, database setup, keychain access, analytics startup, dependency graph construction, parsing, broad registration, or swizzling inside +load should be treated as launch-critical until measurement proves otherwise.

The file is also careful with +initialize. It can delay work until the class receives its first message, but it is not a universal replacement for +load. It can still run early, on a sensitive path, and with ordering or locking surprises. For modern code, explicit initialization or lazy setup is usually easier to reason about.

For Swift code, the reference gives the agent a better rule: look for expensive first access on the launch path. A global or static value may be fine if it is cheap or never touched during startup. It becomes a launch problem when its first access performs I/O, parsing, locking, large allocation, or dependency graph construction before the first frame or first interaction.

The practical review path is to identify the owner of the work, confirm that it really happens before app lifecycle code, and then decide whether it can be removed, reduced, delayed, or made explicit. The agent should use source search and binary inspection as leads, not proof. The final recommendation still needs measurement in the same launch scenario and a release-like build.

SwiftUI app launch

This reference is for SwiftUI lifecycle apps where launch work may hide in @main App, stored properties, App.init, WindowGroup, root view setup, root observable state, environment injection, .task, .onAppear, scenePhase, or @UIApplicationDelegateAdaptor.

The important point is that SwiftUI launch work is often distributed. A UIKit app may have an obvious long didFinishLaunching. A SwiftUI app can spread the same cost across the app type, scene construction, root models, environment values, persistence setup, lifecycle modifiers, and delegate bridging. The code looks declarative, but it can still build a large object graph, touch storage, start network work, resolve dependencies, or block the main actor before the first screen is useful.

The reference treats the SwiftUI App type as a launch boundary. App.init should usually stay small. It can prepare minimal process-level state, but it should not become a new place to put everything that used to live in AppDelegate. Database opening, keychain-heavy checks, remote configuration, SDK startup, large decoding, dependency container construction, and synchronous waits all need a clear reason to run that early.

Root state is another common source of startup cost. A root @StateObject, @Observable, or ObservableObject is not automatically a problem. The risk is expensive initialization, broad dependency construction, early side effects, or heavy first access. The agent should preserve SwiftUI ownership semantics because moving state around can change lifetime, identity, cancellation, scene behavior, and restoration.

Environment injection also gets special attention. Injecting a few lightweight values is fine. Building every service and placing the whole app graph into the environment before the first frame is different. Feature-specific dependencies should usually move closer to the feature boundary, or be represented as factories when laziness is safe.

The reference is careful with .task, .onAppear, and scenePhase. Async does not mean post-launch, and it does not mean off-main. A .task can start while the root scene is becoming visible, compete for CPU, I/O, storage, locks, or main-actor time, and update broad observable state right when the app should become responsive. .onAppear is not a one-time launch hook, and scenePhase should not treat every foreground return as a cold launch.

Hybrid apps need extra care. If a SwiftUI app uses @UIApplicationDelegateAdaptor, the adapted delegate is still part of the launch path. SDK startup, push handling, background task registration, routing, and dependency setup may happen there while similar work also exists in SwiftUI root code.

For an AI agent, this reference is useful because it gives a map of SwiftUI-specific launch surfaces. It helps find duplicated startup paths, classify lifecycle-bound work, and keep recommendations small: reduce eager root setup, split minimal launch state from feature state, add idempotency, move feature work later, or add explicit readiness where the first screen cannot be fully interactive yet.

Third-party SDKs at launch

This reference focuses on vendor SDKs that start during app launch: analytics, crash reporting, ads, attribution, remote config, feature flags, push, consent, security, fraud prevention, logging, monitoring, diagnostics, and app-owned wrappers around those SDKs.

The central question is practical: what breaks if this SDK starts after the first frame or after the first interaction? Sometimes the answer is serious. Crash handling, consent enforcement, security gates, push routing, deep links, fraud checks, or kill switches may have real launch-time requirements. Sometimes the answer is much weaker: old event upload, session enrichment, diagnostics sync, ad preload, or remote refresh can often move later.

The reference does not treat SDK startup as one indivisible call. configure, start, identify, enable collection, flush, sync, upload, and preload can be separate responsibilities. A crash SDK may need a handler installed early, while previous crash upload can wait. An analytics wrapper may need to buffer early events locally, while the vendor network stack starts later. Remote config may need cached defaults for launch, while refresh happens after the first screen is visible.

This is also where correctness matters more than a clean benchmark. Delaying an SDK can break attribution capture, notification routing, consent rules, experiment assignment, crash coverage, fraud checks, or security policy. The agent should check vendor documentation and product requirements before recommending deferral. Vendor docs are a constraint, not the whole design, but unsupported lazy startup modes should not be invented.

The reference also points the agent toward hidden SDK startup. SDK work may begin through eager singletons, static access, dependency container registration, SwiftUI root model creation, environment injection, .task, .onAppear, Info.plist behavior, Objective-C +load, constructors, or app-owned facades whose initializer quietly starts vendors.

The best recommendations are usually phase splits, not blanket delays. Keep the minimal early setup, then defer uploads, network refreshes, enrichment, scans, preloads, maintenance, or feature-specific modules. Post-first-frame work still needs bounds. Starting every SDK in an unstructured task immediately after the first frame can still hurt the first interaction.

For an AI agent, this reference is useful because it makes SDK advice safer. It asks the agent to classify each SDK, separate minimal required setup from deferrable work, preserve correctness, and validate not only launch timing but also event delivery, crash capture, attribution, push routing, consent enforcement, and security behavior.

Conclusion

Launch performance is easy to discuss in vague terms and hard to review safely. A slow launch can come from dyld, static initializers, lifecycle callbacks, SwiftUI root setup, dependency orchestration, SDK startup, linking choices, or measurement confusion. Treating all of that as one “startup is slow” problem usually leads to weak advice.

This skill gives an AI agent a narrower job. First classify the launch scenario. Then find the phase. Then separate first-frame work from first-interaction work, secondary work, lazy feature setup, and maintenance. Only after that should the agent recommend changes.

The references make the skill practical. They prevent common mistakes like blaming dyld by default, comparing cold launch with resume, moving SDKs later without checking correctness, parallelizing startup without dependencies, or trusting a single unexplained number.

For iOS teams, this is where skills become useful. They do not replace profiling, product context, or engineering judgment. They make the agent ask better questions, inspect the right code paths, and keep recommendations tied to evidence.

The full skill is available on GitHub.