androidengineers.Book a session

Performance Optimization

Exercise: Reduce TTI by 30%

exercise60 minHard

TTI (Time to Initial Display) measures the time from app launch to the first meaningful frame. This exercise walks you through a systematic optimization process targeting a 30% reduction.

Baseline Measurement

Before you can optimize, establish a reliable baseline:

# Run 5 cold start measurements and average them
for i in {1..5}; do
  adb shell am force-stop com.example.myapp
  sleep 1  # ensure process is dead
  adb shell am start-activity -W -n com.example.myapp/.MainActivity \
    | grep TotalTime
done
# Example output:
# TotalTime: 1850ms
# TotalTime: 1820ms
# TotalTime: 1910ms
# TotalTime: 1840ms
# TotalTime: 1870ms
# Average: ~1858ms  ← your baseline

Target: 1858 × 0.70 = ~1300ms

Step 1: Identify the Biggest Time Consumers

Capture a Perfetto trace of a cold start (see the Systrace lesson) and look for:

bindApplication
├── ContentProvider.onCreate calls (each SDK adds one)
│   ├── Firebase:        ~80ms
│   ├── WorkManager:     ~45ms
│   └── Analytics SDK:   ~30ms  → Total: ~155ms
├── Application.onCreate
│   ├── Room.build():    ~60ms  (synchronous!)
│   ├── Glide.get():     ~25ms
│   └── Other:           ~40ms  → Total: ~125ms
└── Activity.onCreate
    ├── View inflation:  ~120ms
    └── Data load:       ~200ms → Total: ~320ms

Total identified: ~600ms out of ~1858ms. Optimizing these gets us to ~1258ms — a 32% reduction.

Step 2: Eliminate ContentProvider Overhead

// Disable Firebase auto-init ContentProvider
<provider
    android:name="com.google.firebase.provider.FirebaseInitProvider"
    android:authorities="${applicationId}.firebaseinitprovider"
    android:exported="false"
    tools:node="remove" />

Then init Firebase lazily on first use, or via App Startup:

class FirebaseInitializer : Initializer<FirebaseApp> {
    override fun create(context: Context): FirebaseApp {
        FirebaseApp.initializeApp(context)
        return FirebaseApp.getInstance()
    }
    override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}

Expected gain: ~155ms → ~50ms (App Startup uses a single ContentProvider)

Step 3: Move Room Init Off Main Thread

// ❌ Room.build() called synchronously
class MyApplication : Application() {
    val db = Room.databaseBuilder(this, AppDatabase::class.java, "app.db").build()  // ~60ms
}

// ✅ Lazy + background initialization
class MyApplication : Application() {
    val db: AppDatabase by lazy {
        Room.databaseBuilder(this, AppDatabase::class.java, "app.db")
            .build()
    }
}

// Kick off DB build on a background thread before it's needed:
class AppStartupInitializer : Initializer<Unit> {
    override fun create(context: Context) {
        CoroutineScope(Dispatchers.IO).launch {
            (context.applicationContext as MyApplication).db  // trigger lazy init
        }
    }
    override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}

Expected gain: ~60ms removed from main thread

Step 4: Defer Non-Critical Initialization

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // Critical (must be synchronous):
        CrashReporting.init(this)   // ~10ms — needed for all crashes

        // Non-critical (defer post-first-frame):
        ProcessLifecycleOwner.get().lifecycle.addObserver(
            object : DefaultLifecycleObserver {
                override fun onStart(owner: LifecycleOwner) {
                    owner.lifecycleScope.launch {
                        delay(500)  // wait for first frame to render
                        Analytics.init(this@MyApplication)
                        PushNotifications.init(this@MyApplication)
                    }
                }
            }
        )
    }
}

Expected gain: ~70ms moved out of startup

Step 5: Optimize View Inflation

// Pre-inflate layouts on a background thread using AsyncLayoutInflater
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)  // fast: simple root layout

        AsyncLayoutInflater(this).inflate(R.layout.content_main, binding.container) { view, _, parent ->
            parent?.addView(view)  // add inflated content when ready
            initContent(view)      // now setup RecyclerView, etc.
        }
    }
}

Measure After Each Change

# After each optimization, re-measure:
for i in {1..5}; do
  adb shell am force-stop com.example.myapp && sleep 1
  adb shell am start-activity -W -n com.example.myapp/.MainActivity | grep TotalTime
done

Results Tracker

OptimizationExpected gainActual gain
App Startup (replace ContentProviders)105ms___ ms
Room off main thread60ms___ ms
Defer analytics70ms___ ms
View inflation optimization50ms___ ms
Total285ms (15%)___ ms

If you're still short of 30%, go back to the Perfetto trace and find the next biggest slice. Common missed wins:

  • SharedPreferences reads in onCreate → DataStore
  • Synchronous Retrofit client initialization → lazy init
  • Large image in launch theme → switch to placeholder solid color

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Exercise: Reduce TTI by 30% | Android System Design | Android Engineers