DEV Community

SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on Originally published at mvpfactory.io

Wiring Android's Baseline Profiles to Compose Navigation

---
title: "Baseline Profiles + Compose Navigation: How to Actually Move Your P90 Startup Numbers"
published: true
description: "Most teams measure the wrong startup metric. Here is how Baseline Profiles interact with Compose Navigation's lazy loading to move the number users actually feel."
tags: kotlin, android, performance, mobile
canonical_url: https://mvpfactory.co/blog/baseline-profiles-compose-navigation-p90-startup
---
Enter fullscreen mode Exit fullscreen mode

What You Will Build

By the end of this tutorial you will have a MacrobenchmarkRule that generates real profile coverage across your Compose NavGraph — not just the cold-start path to your home screen — and you will know exactly how to validate the result in APK Analyzer. More importantly, you will understand why median startup improvements look great in CI and do nothing for your app store reviews.

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


Prerequisites

  • Android project with Jetpack Compose and Compose Navigation
  • benchmark:macrobenchmark library configured in a :benchmark module
  • A physical device or emulator for profiling (emulators give directional data; real hardware gives production-representative data)

The ART Compilation Pipeline — What Baseline Profiles Actually Do

ART compiles DEX bytecode through several tiers:

Tier Description Startup Impact
Interpreted Bytecode run at runtime Slowest
JIT Compiled on first execution Moderate
Profile-Guided (Partial AOT) Pre-compiled from .prof rules at install Fast
Full AOT Entire app pre-compiled Fastest (high install cost)

Baseline Profiles target Partial AOT. At install time, dex2oat uses your .prof rules to pre-compile only the hot methods your profile covers. Full AOT is too expensive for Play Store distribution — this tradeoff is intentional and correct.

The catch: only code your profile exercises gets pre-compiled. This is where Compose Navigation creates a subtle trap.


Step 1 — Understand the Lazy Destination Problem

Compose Navigation loads @Composable destinations lazily. Your NavHost registers composables by route string; they are not instantiated until the user navigates there. On a cold start, only your start destination and its dependency graph execute.

A naive benchmark looks like this:

@Test
fun startupBenchmark() {
    measureRepeated(
        packageName = "com.yourapp",
        metrics = listOf(StartupTimingMetric()),
        iterations = 10,
        startupMode = StartupMode.COLD
    ) {
        pressHome()
        startActivityAndWait()
    }
}
Enter fullscreen mode Exit fullscreen mode

This profiles exactly the cold-start path to your start destination. Every other NavGraph destination — detail screen, settings, onboarding — generates zero profile rules. The docs do not mention this, but a profile that stops at your entry point is almost no profile coverage at all.


Step 2 — Exercise Real Navigation Flows

Here is the minimal setup to get this working:

@Test
fun startupWithCriticalNavigation() {
    measureRepeated(
        packageName = "com.yourapp",
        metrics = listOf(StartupTimingMetric(), FrameTimingMetric()),
        iterations = 10,
        startupMode = StartupMode.COLD,
        setupBlock = { pressHome() }
    ) {
        startActivityAndWait()
        device.findObject(By.res("home_tab")).click()
        device.waitForIdle()
        device.findObject(By.res("detail_item")).click()
        device.waitForIdle()
    }
}
Enter fullscreen mode Exit fullscreen mode

Exercise your top 3–5 destinations during measurement. This generates profile rules covering composables across your entire nav graph, not just the entry point.


Step 3 — Validate DEX Layout, Not Just Timing

Beyond compilation tier, Baseline Profiles also drive dex layout — the physical ordering of classes and methods within the DEX file. Methods that execute together at startup are reordered to be contiguous on disk, reducing page fault overhead during class loading.

This effect is most visible on mid-range devices where I/O is the actual bottleneck, not CPU. On devices with slower flash storage, this can move P90 meaningfully while barely touching P50.

Validate it directly: open your APK in Android Studio's APK Analyzer before and after profile integration. If your hot classes are scattered across the DEX post-profiling, your benchmark is not covering the right paths. Fix the instrumentation before touching app code.


Step 4 — Measure the Right Metric

Use both metrics and anchor them to reportFullyDrawn() in your Activity:

  • TTID — time to first frame (often a skeleton or spinner)
  • TTFD — time to reportFullyDrawn() — when content the user cares about is actually visible

Apps built around immediate utility live and die by TTFD. Take HealthyDesk, a break reminder app for developers — a loading spinner does not remind you to stand up. The content has to be there on open. Track P90 TTFD across device tiers in CI, not median TTID on your Pixel 9.


Gotchas

Median startup hides the real problem. The bottom 10% of your users — older devices, limited RAM, cold boot after restart — are the ones leaving one-star reviews. P90 is the metric that users on mid-range hardware actually feel.

Profile coverage gaps are silent. No warning is emitted when a destination goes unprofiled. You have to instrument your benchmark to cover critical flows proactively.

TTID vs TTFD confusion. TTID improves are easy to achieve and easy to misread. If your first frame is a loading spinner, a 30% TTID improvement changes nothing for perceived performance.


Summary

Three things worth actually doing:

  1. Exercise your top 3–5 NavGraph destinations in your MacrobenchmarkRule, not just the cold-start entry point.
  2. Track P90 TTFD, not median TTID — that is what users on mid-range hardware feel.
  3. After applying your profile, open APK Analyzer and confirm hot-path classes are contiguous. If they are not, fix the benchmark instrumentation.

Startup benchmarks feel productive. Make sure you are moving the metric that actually matters.

Top comments (0)