Understanding how app startup works

According to Apple’s guidelines, applications launch should take approximately 400 ms.

When exactly should we measure the app’s startup time? Apple defines the launch time – from application:didFinishLaunchingWithOptions: – as the period that ends when this method completes execution, not when the first screen (such as a splash or login view) is displayed. Knowing this, we can begin our analysis.

Cold start vs warm launch

Launch time may vary depending on whether the app is started from a cold state or a warm state.

A cold start occurs when the user launches the app and it is not running in the background-either because it was terminated, killed by the system, or the device has just been rebooted. In this case, all necessary tasks must be performed, such as dynamic linking of frameworks, static initializations, and other startup routines.

A warm launch happens when the app has already been launched and was placed into the background or suspended. In this state, much of the previous work is still in memory, so the system does not need to perform full initialization again. As a result, the user experiences a faster launch time.

Perwarm up mechanism

With iOS 15, Apple introduced a mechanism called prewarming, which is responsible for launching your app before the user even taps on the app icon. This process happens in the background, without the user’s knowledge—and, in many cases, without the developer being aware of it. As far as we know, developers have no control over whether their app will be prewarmed.
The mechanism works by launching the app in the background up to the pre-main stage. This allows the system to prepare dynamic linking and create the necessary mappings for faster dyld load times when the user actually opens the app.

It is still unclear whether application:didFinishLaunchingWithOptions: is called during prewarming. Apple states that the app is not fully run, but some developers have reported observing different behaviors.
Because of this mechanism, certain issues have appeared in some applications. If you placed logic inside application:didFinishLaunchingWithOptions: that depends on external state or side effects, you might encounter unexpected behavior—such as random user logouts caused by not having access to Keychain or UserDefaults, Live Activities glitches, or inconsistencies in startup-time metrics. In particular, companies that strictly monitor launch performance may notice discrepancies in their recorded app launch data.

Measuring app launches

When it comes to measuring app launch performance, we have several options. The easiest way to check how our app performs is to use Xcode Organizer and review the app launch statistics. This provides a general overview of how the app is behaving in real-world conditions.

I recommend focusing on the slowest device that you support. If you manage to improve launch times on the weakest device, all other devices will benefit as well. For example, reducing launch time from 1.5 seconds to 1.0 second on an older device is a significant improvement. However, reducing launch time from 0.5 seconds to 0.4 seconds on the newest iPhone is far less noticeable for users.

When running your own tests, keep in mind that real users may have cluttered memory, Low Power Mode enabled, or other apps actively running in the background. All of these factors can affect startup performance. That’s why it’s important to regularly monitor and analyze these metrics under realistic conditions.

UITests

To measure our app’s startup time in the development environment, Apple recommends using XCTest. The setup process is fairly straightforward.

import XCTest

final class PerformaceCheckUITests: XCTestCase {
    @MainActor
    func testLaunchPerformance() throws {
        // This measures how long it takes to launch your application.
        measure(metrics: [XCTApplicationLaunchMetric()]) {
            XCUIApplication().launch()
        }
    }
}

⚠️ Important: When measuring startup time, for the most accurate results and values closest to those shown in Xcode Organizer – you should use a real device in Release mode. While the Simulator can be used, it typically adds around 0.3-0.4 seconds to the launch time, which can significantly distort your measurements.

Also, if you are using manual code signing, you will need to generate a provisioning profile for the XCTRunner app. This is a companion app that runs your tests. Make sure to create a provisioning profile specifically for the test target, including the xctrunner prefix in the bundle identifier. Xcode may display a provisioning profile mismatch error at first. However, in many cases, it will still build and run successfully despite the error 🙃.

Metric Kit

Another great tool is MetricKit. It is Apple’s own metrics framework that provides detailed performance data about how your app behaves on specific devices.

The data visible in Xcode Organizer is aggregated and presented as a broader overview. With MetricKit, however, you can collect per-device metrics especially if you forward this data to your own analytics pipeline for further analysis.

When it comes to app launch performance, these are the metrics we are particularly interested in:

{
  "applicationLaunchMetrics": {
    "histogrammedExtendedLaunch": {
      "histogramNumBuckets": 3,
      "histogramValue": {
        "0": {
          "bucketCount": 50,
          "bucketStart": "1000 ms",
          "bucketEnd": "1010 ms"
        },
        "1": {
          "bucketCount": 60,
          "bucketStart": "2000 ms",
          "bucketEnd": "2010 ms"
        },
        "2": {
          "bucketCount": 30,
          "bucketStart": "3000 ms",
          "bucketEnd": "3010 ms"
        }
      }
    },
    "histogrammedOptimizedTimeToFirstDrawKey": {
      "histogramNumBuckets": 3,
      "histogramValue": {
        "0": {
          "bucketCount": 50,
          "bucketStart": "1000 ms",
          "bucketEnd": "1010 ms"
        },
        "1": {
          "bucketCount": 60,
          "bucketStart": "2000 ms",
          "bucketEnd": "2010 ms"
        },
        "2": {
          "bucketCount": 30,
          "bucketStart": "3000 ms",
          "bucketEnd": "3010 ms"
        }
      }
    }
  }
}

In that can we can also see the prewarm up launches which are not visible in Xcode Organizer. It is pretty sweet.

Let me break them a little for you:

histogrammedOptimizedTimeToFirstDraw: A histogram of the different amounts of time associated with prewarmed app launches. Here we can get the info how our app launches when the prewarm up mechansiam was used by the system to prepare the app.

histogrammedTimeToFirstDraw: A histogram of the different amounts of time taken to launch the app. Those are the basic cold start up times.

Also we have histogrammedExtendedLaunch when our app took a little longer to strat note that there is a 30 sec limit of the app to launch if app fails user will get a crash.

histogrammedApplicationResumeTime: And we have metric about resuming app from background.

Soon I will make more in depth article about MetricKit I had some time to play with it.

Instruments

Another tool we can use is Instruments. When profiling our app, we can use a predefined scheme that launches the app and allows us to observe what happens during startup. This helps us analyze performance bottlenecks, track system activity, and better understand how the app behaves during launch.

  1. On the first part we have Initiliztaing process which is not interesting for us because during profiling Instruments are doing their work.
  2. Initializing System is a very important phase during app startup. During this time, dynamic libraries are mapped and loaded into memory. Toward the end of this process, static initializers are also executed, which is something you should carefully examine. Some third-party libraries may use the Objective-C +load() method, which is executed before your AppDelegate is initialized. This means certain code can run even before your app’s main lifecycle begins. Be aware that this can potentially introduce bugs or unexpected behavior – especially if you are not aware that a framework is performing work at this early stage of the launch process.
  3. In my case, the small green section represents UIKit initialization. If your app still relies heavily on UIKit, you might find some work happening during this phase.
  4. The longest sausage is AppDelgeate application:didFinishLaunchingWithOptions. Looking at this method we can find all our intiliztions and see what takes the logest time.
  5. The last step is SwiftUI initliazation.

Here is Apple documentation about app launch sequence.

But what can I do?

When it comes to improving app startup time, there are several paths we can take.
First, remove unnecessary dependencies and carefully track what is happening during launch. Review your application:didFinishLaunchingWithOptions: method to understand how and when your code is executed.

By taking a closer look using OSSignpost(blog post placement), you can uncover valuable insights. Visualizing the initialization time of each process gives you a much clearer picture of what should be optimized.

Also when dealing with older apps and the approach EVERYTHING GO TO APPDELEGATE with the mix of concurrent MainTread with Dispatch Async initialization – I created a sample app that demonstrates the difference between older concurrency patterns – primarily using DispatchQueue.main and DispatchQueue.async and modern Swift Concurrency with TaskGroup.

The first approach uses a mix of DispatchQueue.main and DispatchQueue.async, while the second one uses Swift Concurrency, which better utilizes multiple cores. From this comparison, we can get a clearer idea of how to approach optimizing our app’s startup performance.

Here is a more detailed breakdown showing how long each process took to complete.

We should also interpret these results with a grain of salt but they still give us a useful insight.

When looking at the results, some tasks may individually take slightly longer with Swift Concurrency. However, because they start roughly at the same time and utilize multiple CPU cores, the overall batch of work completes faster. The same principle applies to app startup optimization.

We should also take Low Power Mode into consideration. Not every user has the latest iPhone with a fully charged and healthy battery. Devices in Low Power Mode or with degraded battery health may experience reduced performance, which can impact startup times.

As you can see from the screenshot, when I run the same app in Low Power Mode, the overall time is significantly longer. Interestingly, the percentage difference between the two approaches also increases.

My assumption is that in Low Power Mode, the performance cores are either disabled or temporarily throttled, and more work is shifted to the efficiency cores. iPhones typically have fewer performance cores (e.g., 2 performance cores vs. 4 efficiency cores, depending on the model), so heavily parallelized work may give as startup boost.

That said, most typical iOS apps do not perform extremely heavy computations during startup so the efficiency cores do the work better in parallel rather the old approach.

Conclusion

I hope the information I’ve gathered here helps you discover practical ways to improve your app’s startup time.

Szymon Szysz is a ex-Apple hater and has come a long way. Now making apps for iOS since 2017. Likes good design and amazing UX. Likes to focus on code quality and does not hesitate to say that something “is not yes” (*ponglish, “is not correct”).


Posted

in

,

by