Layout inflation and rendering are two distinct stages where slowness manifests as jank. Optimizing both requires understanding the rendering pipeline and using the right layout tools.
Overdraw
Overdraw occurs when a pixel is drawn more than once per frame. It's wasted GPU work. Visualize it in Developer Options → Debug GPU Overdraw:
| Color | Overdraw | State |
|---|---|---|
| White | 0× | Ideal |
| Blue | 1× | Acceptable |
| Green | 2× | Acceptable |
| Pink | 3× | Investigate |
| Red | 4×+ | Fix required |
Common causes:
- Background set on both Window and layout root
- Nested Views each with their own background
- Card-inside-card layouts with backgrounds
Fix:
<!-- ❌ Window background + root layout background = overdraw -->
<LinearLayout
android:background="@color/white" <!-- redundant if window is white -->
...>
<!-- ✅ Remove redundant background -->
<!-- In theme: <item name="android:windowBackground">@color/white</item> -->
<!-- In layout: no background on root if it matches window background -->
View Hierarchy Flattening
Deep view hierarchies are expensive to measure and lay out. Use ConstraintLayout to express complex layouts in a flat hierarchy:
<!-- ❌ Deep nesting: 5 levels, 4 layouts -->
<LinearLayout> <!-- level 1 -->
<LinearLayout> <!-- level 2 -->
<LinearLayout> <!-- level 3 -->
<FrameLayout> <!-- level 4 -->
<TextView /> <!-- level 5 -->
<!-- ✅ Flat: 1 level, constraints express the same layout -->
<ConstraintLayout>
<TextView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
... />
<ImageView
app:layout_constraintTop_toBottomOf="@id/textView"
... />
</ConstraintLayout>
Lint check: Hierarchy Viewer (deprecated) → Layout Inspector shows the full tree. Use adb shell dumpsys gfxinfo com.example.myapp for measure/layout counts.
ViewStub: Lazy Layout Inflation
For views that aren't always shown (error states, empty states, settings panels):
<ViewStub
android:id="@+id/errorStub"
android:layout="@layout/error_state"
android:inflatedId="@+id/errorView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
// Inflate only when needed
binding.errorStub.inflate() // replaces ViewStub with the actual layout
// Or use visibility-based approach with binding:
binding.errorStub.setOnInflateListener { _, inflated ->
val errorView = ErrorStateBinding.bind(inflated)
errorView.retryButton.setOnClickListener { retry() }
}
Include & Merge
Use <include> to reuse layouts. Use <merge> as the root of included layouts to avoid adding an extra ViewGroup:
<!-- loading_spinner.xml — use <merge> to avoid extra FrameLayout -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ProgressBar ... />
<TextView android:text="Loading..." ... />
</merge>
<!-- In parent layout: -->
<include layout="@layout/loading_spinner" />
Avoid wrap_content on Large Lists
RecyclerView with wrap_content height triggers two measure passes — expensive for long lists. Use match_parent or a fixed height:
<!-- ❌ Two measure passes -->
<androidx.recyclerview.widget.RecyclerView
android:layout_height="wrap_content" />
<!-- ✅ Single measure pass -->
<androidx.recyclerview.widget.RecyclerView
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toBottomOf="parent" />
Layout Inspector: Diagnosing Issues
Android Studio → Layout Inspector while app is running:
- See real-time 3D view of view hierarchy
- Highlight over-deep nesting
- Click any view to see its attributes and measured size
- Rotate the 3D view to see which layers are behind others (overdraw hint)
Key Takeaways
| Optimization | Impact |
|---|---|
| Remove redundant backgrounds | Reduces overdraw from 3× to 1× |
ConstraintLayout flat hierarchy | Reduces measure passes; improves scroll performance |
ViewStub for conditional views | Avoids inflating unused views at startup |
<merge> in <include> | Removes one ViewGroup per reusable layout |
| Fixed RecyclerView height | Prevents double measure pass |