Screen response time. A critical metric for user experience

Anjal Saneen
OkCredit
Published in
9 min readJun 27, 2022

--

A high-quality user experience is always a top priority at OkCredit. We aim to make the mobile app as fast as possible. To better understand performance impact, we measure various L1 and L2 metrics. Among them, a key metric is screen response time. The time it takes the user to reach the first frame of the screen after clicking for opening a screen.

Screen response time is a core web metric for web pages. In addition, Google recently introduced a new metric for assessing full-page responsiveness on the web called Interaction to Next Paint. Android vitals are far behind in terms of measuring responsiveness metrics compared to the web. You can watch the state of responsiveness on the web here. Playstore vitals measure frozen frames and slow frames, but they don’t tell the whole story about the responsiveness of the app. In order for your app to be responsive, you must derive multiple metrics yourself. We share our learnings and findings on-screen response time in this blog post.

How the screens loads

Let me explain briefly how a screen loads on Android before we move on to the metric. For optimal screen load, it’s important that you understand all the operations that occur there. Please keep in mind the term Screen in Android terms as referring to a fragment or activity. We demonstrate the XML and jetpack compose individually below.

XML

We must first understand the principle of XML layout loading, If you dig deep into the Android source code of Activity.setContentView, each layout must go through inflation before it can pass the measure and be drawn.

XML Layout load to draw Phases

1. Load and parse the XML file

loadXmlResourceParser is the method responsible for loading an XML file. We wouldn’t need to go into the implementation details of this method, we just need to know that it reads the XML file we wrote into memory, and performs some data parsing and encapsulation. Therefore, this is a method that requires I/O. As we know, I/O operations are typically more computationally intensive. you see implementation details here.

2. Populate the View tree

you can see the Layout Inflation code here. it’s internally calling createViewFromTag. and it’s triggering createView method. As can be seen from the code inside createView, it creates View instances by reflection. Creating objects through reflection is a relatively more expensive operation.

Inflating an XML file is an IO process and View instances are created through reflection. Due to these 2 operations, XML inflation is expensive. After inflation of layout, it goes to the Measure pass, layout pass, and then drawing the views. I will not go over the details of the Measure pass, Layout pass, and drawing. You can read the details here. The video below provides detailed information at the system level.

Systrace of XML Activity Load
As soon as startActivity opens the Activity call, the app process interacts with AndroidManagerService. AndroidManagerService is in charge of activity launching which is in the System process. So while opening an Activity, the app requires binder transactions, which take very few milliseconds. later it goes to View Inflate, measures traversals, and the first draws. The screenshot below shows a sample XML activity with markings at each step.

Jetpack Compose

XML Loading through I/O and creating classes through reflection cost is saved in Jetpack Compose since layouts are written in Kotlin and compiled just like the rest of your app. Although Jetpack Compose is an unbundled library, it does not benefit from Zygote which preloads the View system’s UI Toolkit classes and drawables. It has to load all the global classes related to compose on the first render. So the startup to the initial compose screen is slow. On the screen which uses compose for the first time in the app, we observed a longer screen loading time. However, subsequent screens load more quickly. We believe the small performance drop is worth the trade-off considering the improvement in developer productivity, experience, and reusability. Taking a tiny bit longer to render is a very small downside in a flood of upsides. Our experience with compose so far has been great too and we would suggest it to any apps. You can go through this link to understand more about each step.

Systrace of ComposeActivity Load
Compose layouts are written in Kotlin and compiled just like the rest of your app. Jetpack compose goes through the three phases for a frame. Composition, Layout, and then Drawing. Below is a screenshot showing a sample compose activity load, including markings at each event.

Screen response time

We wrote our own implementation for instrumenting/measuring screen response. You can use the library below for measuring screen load time.

What is a good Screen response time?

Various research shows the delay of fewer than 100 milliseconds feels instant to a user. In order to provide a good user experience, we should strive to have a duration of 100 milliseconds or less. Below is a good score from the web.

Improving screen load time: Action Items

  • Inflate layout Asynchronously
    We had some critical screens with multiple views which were taking pretty long for inflations. Asynchronously inflating the layout reduced screen load times by 25% and improved frozen frame performance by 30%.
    We Use Systrace/Perfetto to measure inflate time. Android has slices for inflating on Systrace. For layouts with high inflation times, you might consider Asynchronous loading. We found various limitations with AndroidX LayoutInflater. Below is an improved version of AsyncLayoutInflater with coroutine.
  • Flatten your layout.
    The layout goes through measure traversals after inflation of XML. Measuring views determine the sizes and boundaries of view objects. When a layout includes nested hierarchies, the framework may need to iterate several times to resolve portions of the hierarchy that have multiple layers before the elements will be positioned correctly. ConstraintLayout is helpful here. With Constraint Layout, you can create any layout with a flat view hierarchy. You should consider using ConstraintLayout when dealing with large measures. This can be seen in Sytrace/Perfetto. To understand the performance of your layout, you can also extend your root view and measure onMeasure time and number of traversals by wrapping your onMeasure as below on a debug build.
override fun onMeasure(width: Int, height: Int) {
val durationForMeasure = measureTimeMillis {
super.onMeasure(width, height)
}
Log.d("OnMeasure", "Duration=${durationForMeasure}")
}
  • Offload long-running operations from the main thread.
    Use Android Studio CPU Profiler to figure out what is going on in the main thread for local investigation. Use ANR watchdog with a shorter timeout to find long-running operations in production. The ANR watchdog is a simple library that posts messages periodically to the main thread and watches the UI thread block. You should offload all operations except for UI from the main thread. On the debug build, StrictMode can also be enabled to identify long-running threads during development.
  • Lazily initialize objects
    Object creation is a heavy process. When we create a class object, all the public and private properties of that class are initialized inside the constructor. Every variable inside a class initialization requires a certain amount of time to allocate the memory on the heap and hold its reference on the stack. The more variables, the more time it may take. If you have an activity/fragment with a lot of objects being created on initialization use lazy Property in Kotlin for initializing lazily. Lazy initialization is a delegation of object creation when that object is called for the first time. If an object is injected with the dagger inject annotation, use dagger lazy.
  • Use R8 and baseline profile for improving compose load time.
    This is for improving jetpack compose load duration specifically. If your compose app is performing poorly, you might have a configuration problem. Make sure the configurations with R8 and the baseline profile on the production app are correct. Furthermore, you can improve performance by defining your own baseline profiles. You might end up generating an app profile that doesn’t actually improve the performance of your app if you define your own. It’s a good idea to test the profile before shipping it to production. Here’s the video of more performance optimizations on compose.
  • Custom View
    Custom ViewGroup allows you to measure and layout views specific to your use case in an efficient way. You can build a flat hierarchy, minimize the number of measure traversals and optimize memory allocation. Nevertheless, maintaining a codebase with custom views is not easy if you are frequently changing them. We can see how the custom view is used for nearly all views in some of the most performant apps, such as Whatsapp, resulting in low view inflation and a short measure and layout time.
  • Reduce Overdraw
    During the process of displaying the exact layout of the UI in a frame, each pixel can be drawn multiple times. These overdrawn pixels are called overdraw. One example of duplicate work is painting the same color pixels twice. The more layers there are to paint on the screen, the more work the GPU has to do as layers are painted from back to front. This optimization is mostly for low-end devices. On High-end devices overdraw is no longer as significant a problem according to android developer docs. You can visualize overdraw by enabling GPU Overdraw from the developer options. The following example illustrates how overdraw visuals before and after.
  • X2C
    We talked about performance bottlenecks of layout inflation, loading XML into memory through IO operations and parsing and creating View through reflection can be heavy operations. X2C helps us to avoids reading layout files through IO operations at runtime. My personal opinion is that this library may have integration issues and may add a bit to the size of the APK if used in actual projects. In terms of divergent thinking, We believe some of these ideas are still worthwhile exploring.
  • Caching Results in a View Model until the view is ready
    What we observe in the major android architecture patterns is that the ViewModel starts execution only after inflation of the XML view. You can start ViewModel initialisation on activity initialization. Parallelizing this can save time some precious time on on-screen load. We use MVI Architecture. On MVI, UI is represented as the function of a state. That state can be cached until the view is ready and Parallelize this process. With a few changes in the way the screen loads, we were able to save some precious time on the screen.

Summary

The Android Vitals does not provide responsive metrics to track user interaction other than frozen/slow frame rates. To ensure a good user experience, it is crucial to measure user action response times. The most common action is to open a screen. We saw how to measure screen load time and multiple action items to improve screen load time. We still believe that tracking the App’s responsiveness across the board is not sufficient. The Android community needs to develop some better solutions. Please feel free to add your thoughts and suggestions about it.

Targeting 100ms screen load duration also allowed us to reduce frozen frames by 40%. According to Play Vitals, 65% of frozen frames are caused by high input latency for us. We recommend that if your app is also experiencing frozen frames due to high input latency, you can focus on reducing screen load time.

Like any other software product, Performance work is never really “done”. It is an iterative process. Define meaningful metrics, Measure, Profile, Fix Bottlenecks, and Repeat. At some point, you will see that there is a diminishing return on your investment of effort. It would be wise to stop and re-evaluate where you are spending your time if it takes massive effort for a few milliseconds to gain. We hope our experience will be helpful in making your apps faster.

Credits to the entire Android team at OkCredit for making okcredit App faster everyday. Nishant Shah, Rashanjyot Singh, Mohitesh, Harshitfit, Manas Yadav, Pratham Arora, Saket Dandawate, Shrey Garg.

--

--