How to measure and improve your Android App Startup times

Maxi Rosson on 2022-08-05

Explore the different ways to measure and improve your Android App launch times.

Users expect apps to be responsive and fast to load. An app with a slow start time doesn’t meet this expectation and can be disappointing to users. This sort of poor experience may cause a user to rate your app poorly on the Play store, or even abandon your app altogether.

Let’s explore the different ways to measure and improve the App Startup time (AKA launch time or launch performance),

By starting with instrumentation, you can prove there is an opportunity, you can identify where to focus your efforts, and you can see how much you’ve improved things as you start optimizing.

How to measure the Startup Times (locally)

Measuring your app startup time locally on your own device is a good idea when you are implementing optimizations on your app. You can quickly test your changes and measure the impact on startup times.

Time To Initial Display (TTID)

TTID captures the time for your app to draw its background, navigation, any fast-loading local content, placeholders for slower local content or content coming from the network. TTID should be when users can navigate around and get to where they want to go.

In Android 4.4 (API level 19) and higher, Logcat provides a “Displayed” value capturing the time elapsed between launching the process and the completion of drawing the first frame of the corresponding activity on the screen.

The reported log line looks similar to the following example:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

Time To Full Display (TTFD)

TTFD captures the time when your app has completed rendering and is ready for user interaction and consumption, perhaps including content from disk or the network. This can take a while on slow networks and can depend on what surface your users land on.

To instrument Time To Full Display (TTFD), call reportFullyDrawn() in your Activity after all your content is on screen. Be sure to include any content that replaces placeholders, as well as any images you render. Once you instrument calling reportFullyDrawn(), you can see it in Logcat:

ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

ADB

This post from Chet Haase explains how to measure startup times using ADB (Android Debug Bridge):

Testing App Startup Performance Testing launch performance can be tricky, but it doesn’t have to bemedium.com

System tracing

Android offers system tracing that can help to dig deep and diagnose app startup problems.

Jetpack Macrobenchmark

You can set up your application for measuring app startup early with local performance tests by using Jetpack Macrobenchmark: Startup.

Some useful links about the library:

This video and article explain how to measure your app using the Jetpack Macrobenchmark library:

Measure and improve performance with Macrobenchmark Introduction to Jetpack Macrobenchmark and Baseline Profilesmedium.com

How to measure the Startup Times (in production)

Measuring your startup times on production devices is also very important. It offers more accurate measurements because your app is executed in real scenarios and with lots of different devices.

Android Vitals

On the App start-up time page inside Android Vitals (on Google Play console), you can see details about when your app starts slowly from cold, warm, and hot system states. These metrics are automatically calculated, without any development effort.

After reading the Android Vitals support site, you will notice two important aspects to take into account.

The first one is:

If an app starts multiple times on the same day from the same system state, the day’s maximum start-up time is recorded.

This means that only the worst startup time per day is recorded, not any occurrence.

The second one (and more important) is:

Start-up times are tracked when the app’s first frame completely loads, even if it isn’t a screen that users interact with. Example: If an app starts with a splash screen, the startup time equals the time required to display the splash screen.

Those are bad news for apps with a loading or splash screen. Because Android Vitals only measure the time until the loading screen is displayed, instead of the time until the user can interact with the app. So you will see smaller times because they are ignoring your loading screen times.

Firebase Performance Monitoring

Firebase Performance Monitoring automatically collects several traces related to the app lifecycle.

App start trace

This trace measures the time between when the user opens the app and when the app is responsive. In the console, the trace’s name is _app_start. The collected metric for this trace is "duration".

- Starts when the app’s FirebasePerfProvider ContentProvider completes its onCreate method.

- Stops when the first activity’s onResume() method is called.

Note that if the app was not cold-started by an activity (for example, by a service or broadcast receiver), no trace is generated.

For more details about automatic traces, you can read here.

This is an example of how looks the app start time metric in the Firebase Performance Monitoring dashboard:

Firebase Performance Monitoring has the same problem as Android Vitals for apps with a loading screen. In those cases, you are not going to have the real app start time.

Firebase Performance Monitoring + Custom tracing

If your app uses a loading screen, the suggestion is to manually track the startup time on Firebase Performance Monitoring using a custom code trace.

You just need to follow these simple 4 steps:

1. Create a StartupTimeProvider

Content Providers are executed before the application.onCreate, so are a good candidate to start measuring the app startup time.

TheStartupTimeProvider will first initialize Firebase, because that’s needed to start a custom trace on Firebase Performance. Then, it will register an ActivityLifecycleCallbacks to listen for the first non-loading activity onResume call.

package com.dipien.startup

import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log

class StartupTimeProvider: ContentProvider() {

    companion object {
        private val TAG = StartupTimeProvider::class.simpleName
    }

    private val mainHandler = Handler(Looper.getMainLooper())

    override fun onCreate(): Boolean {
        try {
            if (FirebaseApp.initializeApp(context!!) == null) {
                Log.w(TAG, "FirebaseApp initialization unsuccessful");
            } else {
                Log.i(TAG, "FirebaseApp initialization successful");
            }

            StartupTrace.onColdStartInitiated(context!!)
            mainHandler.post(StartupTrace.StartFromBackgroundRunnable)
        } catch (e: Exception) {
            Log.e(TAG,"Failed to initialize StartupTimeProvider", e)
        }
        return true
    }

    override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
        return null
    }

    override fun getType(uri: Uri): String? {
        return null
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return null
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        return 0
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
        return 0
    }
}

2. Configure the StartupTimeProvider on the AndroidManifest

The provider needs to be declared on the AndroidManifest.xml to be invoked on app startup. The initOrder attribute has defined the maximum possible integer, so this provider is the first one to be invoked.

Given that we had to initialize Firebase on the StartupTimeProvider, the FirebaseInitProvider is disabled.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.dipien">

 <application>
  
  <!-- StartupTimeProvider is already initializing Firebase.
   https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
  <provider
   android:name="com.google.firebase.provider.FirebaseInitProvider"
   android:authorities="${applicationId}.firebaseinitprovider"
   tools:node="remove"/>
  
  <provider
   android:authorities="${applicationId}.startup-time-provider"
   android:exported="false"
   android:initOrder="2147483647"
   android:name="com.dipien.startup.StartupTimeProvider"/>

 </application>
</manifest>

3. Create a StartupTrace

The StartupTrace class will start the custom trace and then listen for all the activities onResume. On the first non-loading activity onResume, the trace is stopped.

package com.dipien.startup

import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import com.google.firebase.ktx.Firebase
import com.google.firebase.perf.ktx.performance
import com.google.firebase.perf.metrics.Trace
import java.util.concurrent.TimeUnit

/**
 * A class to capture the Android AppStart Trace information.
 * https://github.com/firebase/firebase-android-sdk/blob/master/firebase-perf/src/main/java/com/google/firebase/perf/provider/FirebasePerfProvider.java
 */
object StartupTrace : Application.ActivityLifecycleCallbacks, LifecycleObserver {
    
    private val TAG = StartupTimeProvider::class.simpleName

    private val MAX_LATENCY_BEFORE_UI_INIT = TimeUnit.MINUTES.toMillis(1)

    var appStartTime: Long? = null
    private var onCreateTime: Long? = null
    var isStartedFromBackground = false
    var atLeastOnTimeOnBackground = false

    private var isRegisteredForLifecycleCallbacks = false
    private var appContext: Context? = null

    private var trace: Trace? = null

    /**
     * If the time difference between app starts and creation of any Activity is larger than
     * MAX_LATENCY_BEFORE_UI_INIT, set isTooLateToInitUI to true and we don't send AppStart Trace.
     */
    var isTooLateToInitUI = false

    fun onColdStartInitiated(context: Context) {
        appStartTime = System.currentTimeMillis()
        trace = Firebase.performance.newTrace("cold_startup_time")
        trace!!.start()

        val appContext = context.applicationContext
        if (appContext is Application) {
            appContext.registerActivityLifecycleCallbacks(this)
            ProcessLifecycleOwner.get().lifecycle.addObserver(this)
            isRegisteredForLifecycleCallbacks = true
            this.appContext = appContext
        }
    }

    /** Unregister this callback after AppStart trace is logged.  */
    private fun unregisterActivityLifecycleCallbacks() {
        if (!isRegisteredForLifecycleCallbacks) {
            return
        }
        (appContext as Application).unregisterActivityLifecycleCallbacks(this)
        isRegisteredForLifecycleCallbacks = false
    }

    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        if (isStartedFromBackground || onCreateTime != null) {
            return
        }
        onCreateTime = System.currentTimeMillis()

        if ((onCreateTime!! - appStartTime!!) > MAX_LATENCY_BEFORE_UI_INIT) {
            isTooLateToInitUI = true
        }
    }

    override fun onActivityStarted(activity: Activity) { }

    override fun onActivityResumed(activity: Activity) {
        if (isStartedFromBackground || isTooLateToInitUI || atLeastOnTimeOnBackground) {
            unregisterActivityLifecycleCallbacks()
            return
        }

        if (activity !is LoadingActivity) {
            Log.d(TAG, "Cold start finished after ${System.currentTimeMillis() - appStartTime!!}ms")
            trace?.stop()
            trace = null

            if (isRegisteredForLifecycleCallbacks) {
                // After AppStart trace is logged, we can unregister this callback.
                unregisterActivityLifecycleCallbacks()
            }
        }
    }

    override fun onActivityPaused(activity: Activity) { }

    override fun onActivityStopped(activity: Activity) { }

    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { }

    override fun onActivityDestroyed(activity: Activity) { }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun onEnterBackground() {
        atLeastOnTimeOnBackground = true
        ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
    }

    /**
     * We use StartFromBackgroundRunnable to detect if app is started from background or foreground.
     * If app is started from background, we do not generate AppStart trace. This runnable is posted
     * to main UI thread from StartupTimeProvider. If app is started from background, this runnable
     * will be executed before any activity's onCreate() method. If app is started from foreground,
     * activity's onCreate() method is executed before this runnable.
     */
    object StartFromBackgroundRunnable : Runnable {
        override fun run() {
            // if no activity has ever been created.
            if (onCreateTime == null) {
                isStartedFromBackground = true
            }
        }
    }
}

4. Add the custom metric on the dashboard

On the Firebase Performance Monitoring dashboard, add the custom metric cold_startup_time.

Now you can see the new metric on the Firebase Performance Monitoring dashboard:

With this custom trace, you are also measuring the time consumed by your loading activity.

How to improve your Startup Times

Once you’ve defined good startup metrics, instrumenting them in your app can allow you to understand and prioritize improving your startup performance to deliver a better user experience.

These are the three more important places you need to inspect in order to implement startup times performance improvements:

Fix startup crashes first

Crashes during startup are the most frustrating and quickest way to get users to abandon your app, so this should be your first priority before starting to reduce startup times.

Background work

Don’t block the main thread unless you have to. Move I/O and non-critical paths work off the main thread. Remove, delay, or move to the background any work that’s not directly related to a startup experience until after the app has started. Some useful tips:

Remove or unify network calls

Apps use to execute lots of network calls when starting. Try to reduce the number of network calls needed to render the first screen or even unify multiple calls, in order to save time.

Be Lazy

Try to do lazy initialization as much as possible in order to execute initializations or configuration as later as possible, on the first usage, inside of Application#onCreate or Activity#onCreate.

Caching content

In some cases, caching the content required to render the first screen can help you to save startup time. You will need to evaluate whether it’s better to optimize for showing fresh content as quickly as possible or to just show what’s available immediately if the network is offline.

Skip debugging code on production

You should skip on production all the code related to debug. Some examples are:

If you have non-production code (for example, debug code) under src/main, your release APK will have it. Organizing your code under the correct source set will help you to reduce your app startup times. You can read more about this topic in the following article:

How to organize your debug and release Android code Organize your Android code on the proper source setsblog.dipien.com

Reduce your Analytics/Tracking libraries integrations

The more libraries you integrate into your app, the longest your startup times will be. Most of the third-party libraries for analytics or tracking include content providers for initialization, causing an increase in your app startup times. Some examples are Firebase Analytics, Firebase Performance Monitoring, Firebase Crashlytics, Sentry, Instabug, Adjust, New Relic, etc.

Try to reduce these libraries to the minimum and avoid integrating multiple libraries that do the same measurements. Consider the sampling as a possibility, so not 100% of your users are affected by the startup times penalties caused by these libraries.

Jetpack App Startup Library

If you use Content Providers to initialize parts of your app, then you should migrate them to the Jetpack App startup library. It provides a performant way to initialize components at application startup. Both library developers and app developers can use this library to streamline startup sequences and explicitly set the order of initialization.

Some useful links about the library:

Disable the FirebaseInitProvider

If your app uses the Firebase SDK, the FirebaseInitProvider is normally automatically merged into your app build by the Android build tools when doing a build with Gradle. Each content provider adds a penalization to your startup times. So, removing this provider can help to your startup performance.

In your manifest, add an entry for FirebaseInitProvider, and make sure to use a node marker to set its tools:node attribute to the value "remove". This tells the Android build tools not to include this component in your app:

<provider
          android:name=”com.google.firebase.provider.FirebaseInitProvider” 
          android:authorities=”${applicationId}.firebaseinitprovider” 
          tools:node=”remove” />

Because you removed FirebaseInitProvider, you’ll need to perform the same init somewhere in your app (in your own Jetpack App Startup Initializer, to ensure Analytics can measure your app correctly):

if (FirebaseApp.initializeApp(context!!) == null) {
    Log.w(TAG, "FirebaseApp initialization unsuccessful")
} else {
    Log.i(TAG, "FirebaseApp initialization successful")
}

More info here.

Custom splash screens

Starting with Android 12, migrating to the SplashScreen API is required. This API enables a faster startup time. The compat library backports the SplashScreen API to enable backward compatibility and to create a consistent look and feel for splash screen display across all Android versions.

See the Splash screen migration guide for details.

Call reportFullyDrawn()

The following article explains a new feature called IORap, introduced in Android 11 to improve application startup times.

Improving app startup with I/O prefetching In Android 11, we introduced IORap, a new feature which greatly improves application startup times. We have observed…medium.com

You can help IORap out by invoking the reportFullyDrawn() callback when your app completes its startup.

Baseline Profiles

Baseline Profiles are a list of classes and methods included in an APK used by Android Runtime (ART) during installation to pre-compile critical paths to machine code. This is a form of profile guided optimization (PGO) that lets apps optimize startup, reduce jank, and improve performance for end users.

See the Baseline Profiles docs for details.

You can also see these official articles and videos about using Baseline Profiles to improve startup times:

Faster Jetpack Compose <-> View interop with App Startup and baseline profile Jetpack Compose is designed to be interoperable with an existing View-based app. This enables you to take an…medium.com

Improving Performance with Baseline Profiles A quick rundown of Baseline Profilesmedium.com

Improving App Performance with Baseline Profiles Or how to improve startup time by up to 40% Posted by Kateryna Semenova, DevRel Engineer; Rahul Ravikumar, Software…android-developers.googleblog.com

Measure and improve performance with Macrobenchmark Introduction to Jetpack Macrobenchmark and Baseline Profilesmedium.com

Improving App Performance with Baseline Profiles Or how to improve startup time by up to 40%medium.com

In this Android Developers Backstage podcast episode they chat about Baseline Profiles:

Roll out your app to Google Play incrementally

Android improves an app’s performance by building a profile of the app’s most important hot code and focusing its optimization effort on it. It relies on the device to optimize apps based on these code profiles, which means it could be a few days before a user sees the benefits.

Android uses the initial rollout of an app to bootstrap the performance for the rest of the users. ART analyzes what part of the application code is worth optimizing on the initial devices, and then uploads the data to Play Cloud. Once there is enough information, the code profile gets published and installed alongside the app’s APKs.

These optimizations help improve cold startup time without an app developer needing to write a single line of code. You just need to roll out your app using alpha, beta, or incremental rollout channels to get this benefit.

You can read more about this in the following article:

Improving app performance with ART optimizing profiles in the cloud In Android Pie we launched ART optimizing profiles in Play Cloud, a new optimization feature that greatly improves the…android-developers.googleblog.com

Lessons learned from important Apps & Libraries

How a single Android developer improved Lyft's Drivers app startup time by 21% in one month Lyft is singularly committed to app excellence. As a rideshare company - providing a vital, time-sensitive service to…android-developers.googleblog.com

Improving App Startup: Lessons from the Facebook App Posted by the Google and Facebook teams. Authored by Kateryna Semenova from the Google Android team and Tim Trueman…android-developers.googleblog.com

How Firebase Performance Monitoring optimized app startup time Mobile users expect their apps to be fast and responsive, and apps with a slow startup time, especially during…firebase.blog

More official tips

In these articles Chet Haase explains some tips to improve startup times:

App Startup, Part 1 Of Content Providers and Automatic Initializationmedium.com

App Startup, Part 2 Lazy Initializationmedium.com

In these Android Performance Patterns videos, Colt McAnlis talks about launch times:

Support Us

There are different ways to support our work: