App startup analysis and optimization

During app startup, your app makes the first impression on users. App startup must be quick to load and display information the user needs to use your app. If your app takes too long to start up, users might exit your app because they are waiting too long.

We recommend using the Macrobenchmark library to measure startup. The library provides an overview and detailed system traces to see exactly what's happening during startup.

System traces provide useful information about what's happening on your device, which helps you understand what your app is doing during startup and identify potential areas for optimization.

To analyze your app startup, do the following:

Steps to analyze and optimize startup

Apps often need to load specific resources during startup that are critical to end users. Non-essential resources can wait to load until after startup completes.

To make performance tradeoffs, consider the following:

  • Use the Macrobenchmark library to measure the time taken by each operation, and identify blocks that take a long time to complete.

  • Confirm that the resource-intensive operation is critical to app startup. If the operation can wait until the app is fully drawn, it can help minimize resource constraints at startup.

  • Ensure that you expect this operation to run at app startup. Often, unnecessary operations can be called from legacy code or third-party libraries.

  • Move long-running operations to the background, if possible. Background processes can still affect CPU usage during startup.

After you fully investigate the operation, you can decide on the tradeoff between the time it takes to load and the necessity of including it in app startup. Remember to include the potential for regression or breaking changes when altering the workflow of your app.

Optimize and re-measure until you're satisfied with the startup time for your app. For more information, see Use metrics to detect and diagnose problems.

Measure and analyze time spent in major operations

When you have a complete app startup trace, look at the trace and measure time taken for major operations like bindApplication or activityStart. We recommend using Perfetto or the Android Studio Profilers to analyze these traces.

Look at the overall time spent during app startup to identify any operations that do the following:

  • Occupy large time frames and can be optimized. Every millisecond counts in performance. For example, look for Choreographer draw times, layout inflation times, library load times, Binder transactions, or resource load times. For a general start, look at all operations that take longer than 20ms.
  • Block the main thread. For more information, see Navigate a Systrace report.
  • Don't need to run during startup.
  • Can wait until after your first frame is drawn.

Investigate each of these traces further to find performance gaps.

Identify expensive operations on the main thread

It's best practice to keep expensive operations such as file I/O and network access off the main thread. This is equally important during app startup, because expensive operations on the main thread can make the app unresponsive and delay other critical operations. StrictMode.ThreadPolicy can help identify cases where expensive operations are happening on the main thread. It's good practice to enable StrictMode on debug builds to identify problems as early as possible, as shown in the following example:

Kotlin

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        ...
        if (BuildConfig.DEBUG)
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectAll()
                    .penaltyDeath()
                    .build()
            )
        ...
    }
}

Java

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        ...
        if(BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                    new StrictMode.ThreadPolicy.Builder()
                            .detectAll()
                            .penaltyDeath()
                            .build()
            );
        }
        ...
    }
}

Using StrictMode.ThreadPolicy enables the thread policy on all debug builds and crashes the app whenever violations of the thread policy are detected, which makes it difficult to miss thread policy violations.

TTID and TTFD

To see the time it takes the app to produce its first frame, measure the time to initial display (TTID). However, this metric doesn't necessarily reflect the time until the user can start interacting with your app. The time to full display (TTFD) metric is more useful in measuring and optimizing the code paths necessary to have a fully useable app state.

For strategies on reporting when the app UI is fully drawn, see Improve startup timing accuracy.

Optimize for both TTID and TTFD, because both are important in their own areas. A short TTID helps the user see that the app is actually launching. Keeping the TTFD short is important to help ensure that the user can start interacting with the app quickly.

Analyze overall thread state

Select the app startup time and look at overall thread slices. The main thread needs to be responsive at all times.

Tools such as the Android Studio Profiler and Perfetto provide a detailed overview of the main thread and how much time is spent in each stage. For more information about visualizing perfetto traces, see the Perfetto UI documentation.

Identify major chunks of main thread sleeping state

If there's a lot of time spent sleeping, it's likely a result of your app's main thread waiting for work to complete. If you have a multithreaded app, identify the thread that your main thread is waiting on and consider optimizing these operations. It can also be useful to ensure there's no unnecessary lock contention causing delays in your critical path.

Reduce main thread blocking and uninterruptible sleep

Look for every instance of the main thread going into a blocked state. Perfetto and Studio Profiler show this with an orange indicator on the thread state timeline. Identify the operations, explore if these are expected or can be avoided, and optimize where necessary.

IO-related interruptible sleep can be a really good opportunity for improvement. Other processes doing IO, even if they're unrelated apps, can contend with the IO that the top app is doing.

Improve startup time

After you identify an opportunity for optimization, explore possible solutions to help improve startup times:

  • Load content lazily and asynchronously to speed up TTID.
  • Minimize calling functions that make binder calls. If they're unavoidable, ensure that you're optimizing those calls by caching values instead of repeating calls or moving non-blocking work to background threads.
  • To make your app startup appear faster, you can display something that requires minimal rendering to the user as quickly as possible until the rest of the screen is finished loading.
  • Create and add add a startup profile to your app.
  • Use the Jetpack App Startup library to streamline the initialization of components during app startup.

Analyze UI performance

App startup includes a splash screen and the loading time of your home page. To optimize app startup, inspect traces to understand the time taken for your UI to be drawn.

Limit work on initialization

Certain frames might take more time to load than others. These are considered expensive draws for the app.

To optimize initialization, do the following:

  • Prioritize slow layout passes and pick these for improvements.
  • Investigate each warning from Perfetto and alert from Systrace by adding custom trace events to reduce expensive draws and delays.

Measure frame data

There are multiple ways to measure frame data. The five main collection methods are:

  • Local collection using dumpsys gfxinfo: Not all frames observed in the dumpsys data are responsible for the slow rendering of your app or have any impact to end users. However, this is a good measure to look at across different release cycles to understand the general trend of performance. To learn more about using gfxinfo and framestats to integrate UI performance measurements into your testing practices, see Fundamentals of testing Android apps.
  • Field collection using JankStats: Collect frame render times from specific parts of your app with JankStats library and record and analyze the data.
  • In tests using Macrobenchmark (Perfetto under the hood)
  • Perfetto FrameTimeline: On Android 12 (API level 31), you can collect Frame timeline metrics from a Perfetto trace to which work is causing the frame drop. This can be the first step to diagnosing why frames are dropped.
  • Android Studio Profiler for jank detection

Check main activity load time

Your app's main activity might contain a large amount of information that is loaded from multiple sources. Check the home Activity layout, and specifically look at the Choreographer.onDraw method of the home activity.

  • Use reportFullyDrawn to report to the system that your app is now fully drawn for optimization purposes.
  • Measure activity and app launches using StartupTimingMetric with the Macrobenchmark library.
  • Look at frame drops.
  • Identify layouts taking a long time to render or measure.
  • Identify assets taking a long time to load.
  • Identify unnecessary layouts that are inflated during startup.

Consider these possible solutions to optimize main activity load time:

  • Make your initial layout as basic as possible. For more information, see Optimize layout hierarchies.
  • Add custom tracepoints to provide more information about dropped frames and complex layouts.
  • Minimize the number and size of bitmap resources loaded during startup.
  • Use ViewStub where layouts aren't immediately VISIBLE. A ViewStub is an invisible, zero-sized View that can be used to lazily inflate layout resources at runtime. For more information, see ViewStub.

    If you are using Jetpack Compose, you can get similar behavior to ViewStub using state to defer loading some components:

    var shouldLoad by remember {mutableStateOf(false)}
    
    if (shouldLoad) {
     MyComposable()
    }
    

    Load the composeables inside the conditional block by modifying shouldLoad:

    LaunchedEffect(Unit) {
     shouldLoad = true
    }
    

    This triggers a recomposition that includes the code inside the conditional block in the first snippet.