Android App Startup Profiling in 2026: Baseline Profiles, R8 Full Mode, and the Cold Start Budget You're Blowing on Hilt

# webdev# programming
Android App Startup Profiling in 2026: Baseline Profiles, R8 Full Mode, and the Cold Start Budget You're Blowing on HiltSoftwareDevs mvpfactory.io

Deep dive into the interaction between Hilt's component initialization, R8 full mode's aggressive inlining, and Baseline Profile generation — showing exactly where cold start time goes, how to measure it with Macrobenchmark's detailed tracing, and the Hilt tricks (lazy injection, @Singleton scope timing, ComponentActivity init order) that can shave 150–300ms off a real app

---
title: "Android App Startup Profiling in 2026: Baseline Profiles, R8 Full Mode, and the Cold Start Budget You're Blowing on Hilt"
published: true
description: "Deep dive into Android cold start profiling with Macrobenchmark, R8 Full Mode, and Hilt lazy injection tricks that cut 150-300ms from your app launch time."
tags: kotlin, android, mobile, architecture
canonical_url: https://mvpfactory.co/blog/android-cold-start-2026-hilt-r8-baseline-profiles
---

## What We Will Build

By the end of this workshop, you will know exactly where your cold start time is going, how to measure it with surgical precision, and which three levers — Macrobenchmark tracing, R8 Full Mode, and Baseline Profiles — compound together to realistically cut 150–300ms from a Hilt-heavy Android app on real mid-range hardware.

Let me show you a pattern I use in every production project.

## Prerequisites

- Android project using Hilt for dependency injection
- Gradle 8.x with AGP 8.x
- A physical device or a managed device configured for benchmarking
- Basic familiarity with Macrobenchmark (we will cover the setup)

---

## The Cold Start Budget Problem

Most teams treat cold start as a "we'll fix it later" concern — until users start churning on the Play Store. Google has published data tying sub-1000ms cold starts to measurably better retention. By 2026, flagship devices are fast, but the long tail of mid-range hardware is where your P95 numbers live.

Here is what a typical Hilt-heavy app costs on a mid-range device:

| Phase | Typical Time |
|---|---|
| Zygote fork + class loading | 80–140ms |
| Application.onCreate() | 60–180ms |
| Hilt component initialization | 50–150ms |
| First `Activity.onCreate()` | 40–80ms |
| First frame rendered | 30–60ms |
| **Total cold start** | **260–610ms** |

Hilt's share of that budget is larger than most teams realize. Let's measure it first.

---

## Step 1: Instrument With Macrobenchmark Tracing

Before you optimize anything, you need data. Add a `macrobenchmark` module to your project and write a startup test that pairs `StartupMode.COLD` with Perfetto trace capture:

Enter fullscreen mode Exit fullscreen mode


kotlin
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()

@Test
fun coldStartup() = benchmarkRule.measureRepeated(
    packageName = "com.example.app",
    metrics = listOf(StartupTimingMetric(), TraceSectionMetric("Hilt_Init")),
    iterations = 10,
    startupMode = StartupMode.COLD
) {
    pressHome()
    startActivityAndWait()
}
Enter fullscreen mode Exit fullscreen mode

}


Then add a custom trace section inside your `Application.onCreate()`:

Enter fullscreen mode Exit fullscreen mode


kotlin
override fun onCreate() {
Trace.beginSection("Hilt_Init")
super.onCreate() // This triggers Hilt's ApplicationComponent
Trace.endSection()
}


Pull the trace into Android Studio's Profiler or [Perfetto UI](https://ui.perfetto.dev). You will immediately see the `Hilt_Init` slice — and in most production apps, it is embarrassingly wide.

---

## Step 2: Enable R8 Full Mode (And Understand Its Limits)

Add this to `gradle.properties`:

Enter fullscreen mode Exit fullscreen mode


properties
android.enableR8.fullMode=true


R8 Full Mode enables aggressive inlining, class merging, and unused code removal that the default mode skips. For Hilt specifically, it eliminates generated factory boilerplate and inlines single-use providers.

Here is the gotcha that will save you hours: **R8 Full Mode reduces bytecode size and improves JIT compilation speed, but it does not change *when* your object graph is constructed.** Eager `@Singleton` bindings still execute on the main thread during `Application.onCreate()`. R8 makes each individual construction cheaper — it does not defer it. You still need the Hilt tricks below.

---

## Step 3: Apply the Hilt Tricks That Actually Move the Needle

### Lazy injection for non-critical singletons

Replace direct injection with `Lazy<T>` for anything not needed on the first frame:

Enter fullscreen mode Exit fullscreen mode


kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// Eager — costs time at onCreate()
@Inject lateinit var analyticsManager: AnalyticsManager

// Lazy — zero cost until first access
@Inject lateinit var reportingService: Lazy<ReportingService>
Enter fullscreen mode Exit fullscreen mode

}


Audit every `@Inject` in your `Application` and `MainActivity`. Anything not needed before the first frame is a candidate for `Lazy<T>`.

### @Singleton scope timing audit

Not all `@Singleton` bindings are equal. Use the Perfetto trace to identify which `@Provides` methods execute during startup and which ones pull in transitive dependencies — network clients, database builders, analytics SDKs.

Move heavyweight initializations behind a `@Singleton` + `Lazy` combination and initialize them off the main thread post-first-frame:

Enter fullscreen mode Exit fullscreen mode


kotlin
override fun onCreate() {
super.onCreate()
// After first frame, initialize heavy dependencies off-thread
coroutineScope.launch(Dispatchers.IO) {
reportingService.get() // triggers initialization safely off main thread
}
}


### ComponentActivity init order

The docs do not make this obvious, but `ComponentActivity` triggers `HiltActivityComponent` construction earlier than most developers expect — before your own `setContent {}` call. If you are injecting ViewModel factories or SavedState handles, this component builds eagerly. Consider splitting your Hilt entry point setup to delay non-essential component wiring until after `onStart()`.

---

## Step 4: Ship a Baseline Profile With Every Release

Baseline Profiles pre-compile critical execution paths via ART's AOT pipeline, moving work from runtime JIT to install time. Here is the minimal setup to get this working:

Enter fullscreen mode Exit fullscreen mode


kotlin
@OptIn(ExperimentalBaselineProfilesApi::class)
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()

@Test
fun generate() = baselineProfileRule.collect(
    packageName = "com.example.app"
) {
    startActivityAndWait()
    // Add your key user journeys here
}
Enter fullscreen mode Exit fullscreen mode

}


The resulting `baseline-prof.txt` ships with your APK. On first launch, ART skips JIT for covered paths entirely. Combined with lazy injection and R8 Full Mode, this is where the 150–300ms gains become real.

Commit `baseline-prof.txt` to source control and validate it has not regressed with each CI build. It is free performance at install time.

---

## Gotchas

- **R8 Full Mode can break reflection-heavy libraries.** Always run your full test suite after enabling it. Check the [R8 compatibility guide](https://developer.android.com/build/shrink-code) for known issues.
- **Macrobenchmark requires a release or `benchmark` build variant.** Running it on a debug build gives you meaningless numbers — debug builds disable R8 and ART optimizations.
- **`Lazy<T>` is not thread-safe by default in Hilt.** If you access a lazy dependency from a background thread, ensure your injection point is scoped correctly.
- **Baseline Profiles need regeneration after significant code changes.** Stale profiles can cover paths that no longer exist or miss new critical paths added since last generation.
- **`ComponentActivity` init order surprises.** If migrating from `AppCompatActivity`, re-audit your injection points — the component lifecycle timing differs.

---

## Conclusion

Measure before you optimize. Use `Macrobenchmark` with `TraceSectionMetric` and Perfetto to pinpoint exactly what Hilt is constructing during your cold start window. Guessing costs more time than instrumenting.

Audit every `@Inject` in `Application` and `MainActivity`. Replace non-critical dependencies with `Lazy<T>` and move heavy I/O initialization off the main thread. This single change is usually responsible for the biggest gains.

Ship a Baseline Profile with every release. Generate it against your real startup journeys, and validate it in CI. It is the multiplier that makes everything else compound.

**Further reading:**
- [Macrobenchmark docs](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview)
- [Baseline Profiles guide](https://developer.android.com/topic/performance/baselineprofiles/overview)
- [R8 full mode reference](https://developer.android.com/build/shrink-code)
- [Perfetto UI](https://ui.perfetto.dev)
Enter fullscreen mode Exit fullscreen mode