How We Improved Our Android App “Cold Start” Time by 28%

Four of our biggest and best improvements shaved off a full second of app launch time.

Anshu Rustagi
Code Red

--

Since Q1 of this year, Redfin’s Search Apps team has spent a lot of time measuring and improving the performance of our mobile apps for iOS and Android. We improved our “cold start” time by 28%, following a few basic techniques.

new on the left; old on the right

Defining Cold Start

A cold start occurs when a user launches the app and doesn’t have a previous session running in the background. We chose to focus on cold start time because we believe it significantly affects user experience (as measured by user retention).

We looked at two different types of cold start for our Android app: launching to the “Map Screen” and the “Home Details Screen.”

When users launch our app from their device’s home screen, we start by showing our “Map Screen,” a map of nearby homes for sale. Users can then tap on a particular home to visit our “Home Details Screen,” where we display all the details about that particular home.

When measuring a cold start to the Map Screen, we measure it by killing the app and launching it from the device home screen. We define the cold start as complete when the map is loaded; this does not include time taken for the network request for homes for sale.

Our app cold starts to the Home Details Screen when opening a notification or deep link into the app, and ends when the critical “above the fold” home details are loaded, including price, beds, baths, etc. This includes the request for fetching listing data.

We’ll be focusing on cold start to the Map Screen in this blog post. Most of our improvements discussed here apply to cold start to the Home Details Screen as well.

Show Me the Money!

For the last couple of quarters, we’ve been tracking our cold start time in statsD. Here’s a look at where we are now, tracking our median and upper 90 for a recent week:

click to zoom in!

This is a huge improvement from where we were in Q1. To highlight that, here’s a summarized comparison:

                Q1      Q3      % Improvement
---- ---- -------------
Upper 90 5.3s 3.8s 28.30%
Median 2.4s 1.75s 27.08%

As long as our app launches to the map screen, it’s really a wrapper around the Google Maps SDK. This makes the Google Maps app a great benchmark and one we should strive to match. On the Nexus 5x, we went from being ~157% of their cold start time to ~112%.

What Improvements Did We Make?

Here’s a rundown of some of the biggest wins we had for improving our performance. While there was some low hanging fruit in improving our performance, many of biggest gains came from larger projects that took several weeks. The improvement times shown are against our upper 90 statsD numbers.

App State Deserialization Improvements — 500ms improvement

When starting our app, we first deserialize our AppState. This contains all of the persisted data we need to relaunch the app with a consistent experience for our users. It contains everything from feature toggles (we call them “bouncers”) to login status to tracking data; our whole app relies on its availability. As you can imagine, this can be a ton of data. We made two improvements to speed up deserializing all this data:

  1. Switch from Java binary to JSON serialization. It’s much faster and also harder to break backward compatibility.
  2. Deserialize this data in parallel. We already split this data up into different files so that an error in a single file doesn’t break the entire AppState. Now, we read all of this data in parallel instead of serially.

Switching to Dagger for Dependency Injection — 300ms improvement

For many years we used Roboguice for Dependency Injection in our Android app. This is a version of the familiar Guice library, optimized for Android. However, Roboguice relies on a lot of reflection for injection, causing it to be really slow on mobile apps. Roboguice also ceased active development last year and is no longer a supported library.

We’ve since switched to Dagger, which is currently the most popular Android DI library. It focuses on a lot more compile time code generation, skipping the need for reflection, and is thus a lot more performant.

Caching Analytics Tracking Data — 300ms improvement

We found that we were querying for the Device ID on every app start to use in our marketing tracking. These IDs rarely change and we were able to make a huge improvement by just locally caching IDs and querying for any updates in a background process.

Delay Inflating Below-the-Fold Content — 500ms improvement to the Home Details Screen

Our Home Details Screen is a really long ScrollView that showcases a ton of content about the home. We used to inflate all of the views on this screen at once, which turned out to be really slow. To improve on this, we instead decided to split the content into an “above the fold” section and a “below the fold” section. Our above-the-fold content contains the views shown at the top of the screen that we want the user to be able to interact with as soon as possible, including the photo gallery, price, beds, baths, etc. Our below-the-fold content contains all of the other views, including the listing description, a map of the neighborhood, property history and much more.

By splitting our views up into these two categories, we were able to inflate them separately. We still inflate the above the fold content synchronously when the Fragment holding our page is created. However, we were able to use AsyncLayoutInflater to inflate our below-the-fold content in a background thread and then post back to the main thread to render when it was ready for the user to see. This way the below-the-fold content was no longer slowing down showing our most important content to the user.

What’s Next?

Based on our current cold start benchmarking we feel pretty comfortable with the current state of our cold start to Map Screen time. We will continue monitoring this area to avoid regressions, but don’t have any further work planned. In the future, we may identify other areas for performance improvements (e.g. search time, memory usage, etc.).

--

--