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
| Optimization | Expected gain | Actual gain |
|---|---|---|
| App Startup (replace ContentProviders) | 105ms | ___ ms |
| Room off main thread | 60ms | ___ ms |
| Defer analytics | 70ms | ___ ms |
| View inflation optimization | 50ms | ___ ms |
| Total | 285ms (15%) | ___ ms |
If you're still short of 30%, go back to the Perfetto trace and find the next biggest slice. Common missed wins:
SharedPreferencesreads inonCreate→ DataStore- Synchronous Retrofit client initialization → lazy init
- Large image in launch theme → switch to placeholder solid color