Android 15 startup time: profile the WindowInsets tax
Meta description: Android 15 forces edge-to-edge layouts and Predicted Back pre-warm, adding measurable latency to startup. Profile the WindowInsets cost with Perfetto.
TL;DR
Android 15’s mandatory edge-to-edge enforcement (API 35+) and Predicted Back Gesture animations introduce a serialized insets negotiation pass that adds measurable latency to your app’s time-to-first-frame. This isn’t a bug — it’s a structural cost. Profiling with Perfetto isolates exactly where your Activity.onStart() stalls waiting for the WindowInsets chain to resolve before the first frame can commit.
The problem most teams don’t see coming
Most teams benchmark their release build, see acceptable startup numbers on Android 14, ship to production, and then watch p95 startup times spike on devices running Android 15. The culprit isn’t your code — it’s a new mandatory synchronization point the OS introduces before your first frame renders.
In my experience building production systems on Android, insets-related layout invalidations are among the most invisible sources of jank. Android 15 made them mandatory and earlier in the startup sequence. The impact is most pronounced on mid-range hardware with deep ViewGroup hierarchies during cold starts — the conditions that reflect your real user base, not your benchmark device.
What changed in Android 15
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, which means the system must negotiate insets — status bar height, navigation bar height, display cutout — with your ViewGroup hierarchy before layout can commit.
This negotiation happens via View.onApplyWindowInsets(), and it propagates depth-first through your entire view tree. If you haven’t 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 — either the previous Activity or the home screen animation — before the user lifts their finger. For your incoming Activity, this means 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.
Profiling with Perfetto
Run this to capture startup timing:
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
On the main thread, look for these key slices. Names are sourced from AOSP — verify the exact strings in your trace, as 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 gap between ActivityThread#handleResumeActivity completing and Choreographer#doFrame firing is your startup tax. A large gap here almost always contains a blocking performTraversals triggered by a late insets callback — the main thread stalls waiting for insets before layout can commit.
The root cause: insets arrive after layout starts
This article is scoped to the View system. If you are building with Jetpack Compose, the equivalent is Modifier.windowInsetsPadding() via the WindowInsets APIs — Compose’s recomposition model avoids the double-traversal problem described here.
For View-based apps, the pattern matters:
// 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.
Comparison: Android 14 vs Android 15 startup behavior
| 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 |
What to do about it
-
Register your
WindowInsetsCompatlistener before the first layout pass completes. Set it up inonCreate()aftersetContentView(), apply padding immediately viainsets.getInsets(WindowInsetsCompat.Type.systemBars())andview.updatePadding(...), and the first traversal has correct dimensions — no relayout needed. -
Add a Perfetto startup trace to your CI pipeline and gate on the delta between
ActivityThread#handleResumeActivityand firstChoreographer#doFrame. A regression on Android 15 that doesn’t appear on Android 14 almost always points to an insets serialization issue in a deepViewGrouphierarchy. -
Audit your
onApplyWindowInsetsimplementations for layout invalidations. CallingrequestLayout()inside an insets callback guarantees a double-traversal on Android 15. UseupdatePaddingextensions fromandroidx.coreinstead — they’re invalidation-safe and coalesce correctly with the existing traversal pass.
Tags: android kotlin mobile architecture startup