Dev.to WebDev πŸ›  Dev πŸ‘ 1 πŸ“– 4 min read

App Startup Time on Android 15

--- title: "Android 15 Startup Time: Profiling the WindowInsets Tax" published: true description: "Android 15's mandatory edge-to-edge enforcement and Predicted Back pre-warm add measurable latency to app startup. Here i

---
title: "Android 15 Startup Time: Profiling the WindowInsets Tax"
published: true
description: "Android 15's mandatory edge-to-edge enforcement and Predicted Back pre-warm add measurable latency to app startup. Here is how to profile the WindowInsets cost with Perfetto and fix it."
tags: [android, kotlin, mobile, performance]
canonical_url: https://blog.mvpfactory.co/android-15-startup-time-windowinsets-tax
---

## What We Will Build (and Fix)

By the end of this tutorial you will know how to capture a Perfetto startup trace, identify the serialized `WindowInsets` negotiation pass that Android 15 introduces, and apply the one-line registration pattern that eliminates the double layout traversal before your first frame commits.

If your p95 startup times spiked after targeting API 35, this is the post you needed two sprints ago.

---

## Prerequisites

- Android Studio Hedgehog or later
- A device or emulator running Android 15 (API 35)
- ADB in your `PATH`
- A View-based Activity (Compose handles this differently β€” more on that below)

---

## Step 1 β€” Understand What Changed

### Edge-to-edge is no longer optional

Targeting API 35 forces `WindowCompat.setDecorFitsSystemWindows(window, false)` behavior system-wide. Your layout now owns the full screen. That means the OS must negotiate insets β€” status bar height, navigation bar height, display cutout β€” with your entire `ViewGroup` hierarchy before layout can commit.

This negotiation propagates depth-first through every `View.onApplyWindowInsets()` in your tree. If you have not adopted `WindowInsetsCompat` properly, this triggers additional layout passes.

### Predicted Back Gesture pre-warm

The Predicted Back Gesture, enforced in Android 15, requires the system to pre-warm the back destination before the user lifts their finger. For your incoming Activity, the system may begin a lightweight initialization pass of your window earlier than `onStart()` to support smooth transition previews.

The cost: an additional `WindowInsets` measurement pass during this pre-warm phase can serialize against your main thread layout. The impact is most visible on mid-range hardware with deep `ViewGroup` hierarchies during cold starts β€” the exact conditions that reflect your real user base.

---

## Step 2 β€” Capture a Perfetto Trace

Here is the minimal setup to get this working.


bash
adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/startup.pftrace \
<<EOF
buffers: { size_kb: 63488 }
data_sources: { config { name: "track_event" } }
data_sources: { config { name: "android.surfaceflinger.frametimeline" } }
duration_ms: 5000
EOF


Launch your app during the capture window, then pull the trace and open it at [ui.perfetto.dev](https://ui.perfetto.dev).

---

## Step 3 β€” Find the Startup Tax

On the main thread, look for these slices. Verify exact names in your trace β€” they can vary by Android version:

| Slice Name | What It Represents |
|---|---|
| `ActivityThread#performLaunchActivity` | Activity creation through `onCreate()` |
| `ActivityThread#handleResumeActivity` | Resume phase through `onResume()` |
| `ViewRootImpl#performTraversals` | Full layout/measure/draw pass |
| `Choreographer#doFrame` | First frame commit to SurfaceFlinger |

The number to watch is the **gap between `ActivityThread#handleResumeActivity` completing and `Choreographer#doFrame` firing**. That gap is your startup tax. A large gap almost always contains a blocking `performTraversals` triggered by a late insets callback β€” the main thread stalls waiting for insets before the first frame can commit.

---

## Step 4 β€” Apply the Fix

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


kotlin
// WRONG β€” triggers relayout when insets arrive after first traversal
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// First layout pass commits without insets; a relayout follows
}

// RIGHT β€” listener registered before the first traversal completes
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updatePadding(
left = bars.left,
top = bars.top,
right = bars.right,
bottom = bars.bottom
)
insets
}
}


When insets arrive after your first `performTraversals`, Android invalidates and re-measures the entire hierarchy β€” adding a full second layout pass before the frame is ready to commit. Register the listener in `onCreate()` after `setContentView()` and the first traversal has correct dimensions.

---

## Gotchas

Here is the gotcha that will save you hours.

**Calling `requestLayout()` inside an insets callback guarantees a double-traversal on Android 15.** Use `updatePadding` extensions from `androidx.core` instead. They are invalidation-safe and coalesce correctly with the existing traversal pass.

**Compose is a different story.** If you are building with Jetpack Compose, `Modifier.windowInsetsPadding()` via the `WindowInsets` APIs avoids the double-traversal problem entirely. This post is scoped to the View system.

**Android 14 baselines will not catch this.** The behavior changed structurally between versions:

| Behavior | Android 14 | Android 15 (API 35+) |
|---|---|---|
| Edge-to-edge | Opt-in | Mandatory |
| Insets negotiation timing | Post-layout optional | Pre-frame required |
| Predicted Back pre-warm | Optional gesture nav | Enforced system-wide |
| Relayout risk on late insets | Medium | High |

**Add Perfetto gating to CI.** Gate on the delta between `ActivityThread#handleResumeActivity` and first `Choreographer#doFrame`. A regression on Android 15 that does not appear on Android 14 almost always points to an insets serialization issue in a deep `ViewGroup` hierarchy.

---

## Conclusion

Android 15's mandatory edge-to-edge enforcement and Predicted Back pre-warm are not bugs β€” they are structural costs that serialize against your main thread layout before the first frame commits. The docs do not make this obvious, but Perfetto will show it immediately.

The fix is straightforward: register your `WindowInsetsCompat` listener before the first traversal, use `updatePadding` from `androidx.core`, and add a startup trace to your CI pipeline so regressions surface before they reach your users.

**Further reading:**
- [WindowInsetsCompat reference](https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat)
- [Edge-to-edge migration guide (API 35)](https://developer.android.com/develop/ui/views/layout/edge-to-edge)
- [Perfetto documentation](https://perfetto.dev/docs/)
- [Predicted Back Gesture overview](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture)
πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.