How We’ve Optimized Fiverr’s iOS App Launch Time

Stas Keiserman
Fiverr Tech
Published in
6 min readJul 29, 2020

We all know how important first impressions are. Be it on a date or in business, the first positive experience has a great impact on success. Our apps are no different.

When a user downloads an app from the App Store, it’s just like opening a Christmas present. It’s the same feeling of excitement when opening the app and getting all of that good content as quickly as possible.

So how disappointing is it, when instead of beautiful animations and colorful design, you’re staring at a splash screen for a few seconds.

And that’s it, your first impression is ruined. Everything that comes after it, from how satisfied you are with the app to your review in the store, is going to be impacted.

The good news is that it’s something that can be fixed quite easily, and it only took Fiverr a week to improve our App launch considerably.
In this blog, we are going to discuss how iOS apps startup works, how you can improve your App launch time, and how to maintain it.

How To Measure App Launch Time:

First, let’s look at the various types of App launches:

  • Cold Launch — App is not contained in memory after restart or extensive time unused. Launch will take the longest
  • Warm Launch — Subsequent launch of App will be done from memory. Launch will be faster than Cold Launch
  • Hot Launch — App is in background and moved to foreground

We will focus primarily on how to optimize the Cold and Warm launches, as they have the most effect on time. App Launch is comprised of two phases:

  1. Pre-Main:
    Before didFinishLaunchingWithOptions() is called. We’ve done a lot of work here on how we structure our dependencies and our usage of Obj-C.
  2. Post-Main:
    This is the time after UIApplicationMain is returned and the OS gives “control” back to us.
  3. In the end we get the following simple equation:
    Pre-Main + Post-Main = App Launch Time

Now let’s take a closer look at the Pre-Main phase, and what it consists of:

  1. Dylib loading (DYLD3) — time it takes to load dependent dynamic libraries used by the app.
  2. Rebase / Binding — time it takes adjusting pointers within an image (Rebasing) and setting pointers to point to symbols outside the image (Binding). Strictly related to ObjC and how it prepares its runtime.
  3. ObjC setup (influenced by #2) — time it takes for class registration, category registration and selector uniquing. Any improvements made to #2 will directly influence this category.
  4. Initializer — time it takes to run ObjC static initializers.

The fastest way to measure Pre-Main times, is to add an Environment Variable called DYLD_PRINT_STATISTICS to the Run in your Project Scheme, and give it the value of 1, as follows:

Now run your app, and look at the console. You should see something similar to the following output:

Total pre-main time: 2.4 seconds (100.0%)
dylib loading time: 1.57 seconds (65.4%)
rebase/binding time: 595.26 milliseconds (24.7%)
ObjC setup time: 226.2 milliseconds (9.8%)
initializer time: 9.20 milliseconds (0.03%)

You can see all the things we’ve talked about and how much time they take.

How We Improved Our Launch Time:

We ran an initial test using the aforementioned DYLD_PRINT_STATISTICS on our App, using the oldest device we supported at the time, iPhone 5S. We got the following results:

Total pre-main time: 2.3 seconds (100.0%)
dylib loading time: 1.6 seconds (71.6%)
rebase/binding time: 171.17 milliseconds (7.2%)
ObjC setup time: 147.21 milliseconds (6.2%)
initializer time: 352.04 milliseconds (14.9%)

You can see that our Pre-Main time was horrible. It took 2.3 seconds before our App even got control, so the overall experience was unsatisfying to say the least. We could see from the output that 65% of the time went into Dylib, so that’s where we started our optimization journey.

Static Linking

Our project uses CocoaPods for dependency management. We took a look at how many pods we were loading, and we saw the staggering number of 57 (Apple recommends up to 6). What was more disturbing was that they were all dynamically linked, which in contrast to static linking, has a high performance hit. That started explaining the Dylib loading times we’d been seeing.

We decided to try and use static linking to see if we could gain some improvement. Unfortunately, back then (Xcode 10 had just come out) we ran into problems with the new build system that Xcode 10 came with, and with CocoaPods when trying to convert to static linking. The issue was with us having both ObjC and Swift pods, who didn’t want to play nicely together when we tried to statically link them. So we took out all the ObjC pods from CocoaPods, and inserted them into the project manually. Afterwards we had no issues with statically linking all the remaining Swift pods. We ran the test again and got:

Total pre-main time: 1.5 seconds (100.0%)
dylib loading time: 0.75 seconds (50.0%)
rebase/binding time: 186.72 milliseconds (12.4%)
ObjC setup time: 128.32 milliseconds (8.5%)
initializer time: 433.55 milliseconds (28.9%)

We’d just got 800ms improvement! What we saw was that Dylib time had been significantly reduced, but all other ObjC related stats had risen. We’ll talk about them next.

ObjC Optimizations

As we’ve seen, all stats that were related to how ObjC operates had risen, and that’s because we’d moved a lot of Pods to the project and added them manually. While the performance hit from ObjC initialization was negligible, when compared to the gain from static linking, we were still eager to improve that as well. We analyzed our ObjC code, and found out that 60% could be rewritten in Swift within a number of days. We went for the low hanging fruit. We ran the test again and got:

Total pre-main time: 1.22 seconds (100.0%)
dylib loading time: 0.75 seconds (61.4%)
rebase/binding time: 91.27 milliseconds (7.4%)
ObjC setup time: 86.95 milliseconds (7.1%)
initializer time: 287.14 milliseconds (23.5%)

An improvement of 280ms, not bad at all!

Post-Main Improvements

We started by running the instrument App Launch, and timing how long it took the App to take control until the first UI appeared. Apple’s recommendation is 400ms. We got 627ms. Not great, not terrible. We started digging into the analysis.

First thing we noticed was that we had a lot of Singletons and Managers getting initialized and doing work in AppDelegate, but that weren’t actually used during App startup or in any of the UI screens at the beginning. We also noticed that we were accessing the KeyChain and reading from disk two files in a synchronous way, and it was very time consuming.
After a refactor, we had reduced the time to 327ms, which apart from being noticeable visually, was also more architecturally correct.

Coming back to our formula, we got:
1.22s + 0.327s = 1.547s app startup time, compared to the original 2.927s

Wrapping Up

We’ve come to the end of our optimization journey, and as you can see, we’ve done some remarkable work.
By utilizing the tools and guidelines that Apple provided us, we were able to analyze and detect all the issues we had with our startup code, and improve it considerably. Thus enhancing the experience for the user, and giving them back that excitement we talked about in the beginning.
There’s nothing more satisfying at the end of such challenging work, than to receive hundreds of positive reviews on the App Store, where users commented on how they felt the change in the App’s performance and how fast everything feels now. And that’s the power of first impressions.

--

--