Layout Performance Fundamentals
How Android Renders Frames
Android targets 60 frames per second (16.6ms per frame). Each frame goes through three phases:
- Measure: Each view determines its size based on parent constraints
- Layout: Each view positions itself within its parent
- Draw: Each view renders its pixels
If any frame takes longer than 16.6ms, the frame is dropped and the user sees jank. Layout performance directly affects the first two phases.
The Cost of Deep Hierarchies
Every view in the hierarchy participates in measure and layout. A deep nesting means more measure passes, more layout passes, more objects in memory, and slower inflation time.
<!-- BAD: Deeply nested hierarchy -->
<LinearLayout>
<LinearLayout>
<RelativeLayout>
<FrameLayout>
<TextView />
</FrameLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<!-- GOOD: Flat hierarchy with ConstraintLayout -->
<ConstraintLayout>
<TextView
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintLayout>
ConstraintLayout: The Flat Hierarchy Solution
ConstraintLayout expresses complex layouts in a single nesting level:
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/avatar"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/like_button"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="12dp" />
<ImageButton
android:id="@+id/like_button"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</ConstraintLayout>
Measuring Layout Cost
You can measure layout performance programmatically:
// Measure inflation time
val startTime = System.nanoTime()
val view = layoutInflater.inflate(R.layout.complex_layout, parent, false)
val endTime = System.nanoTime()
Log.d("LayoutPerf", "Inflation: ${(endTime - startTime) / 1_000_000}ms")
// Measure a layout pass
view.doOnLayout {
val measureStart = System.nanoTime()
view.measure(
View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(view.height, View.MeasureSpec.EXACTLY)
)
val measureEnd = System.nanoTime()
Log.d("LayoutPerf", "Measure: ${(measureEnd - measureStart) / 1_000}us")
}
Common Layout Anti-Patterns
1. Nested LinearLayout with weight: The layout_weight attribute forces a second measure pass. Avoid it in RecyclerView items.
2. Invisible views still measured: Views with android:visibility="invisible" are still measured and laid out. Use gone to skip them entirely.
3. Unused view stubs: Use <ViewStub> for layouts that may not be displayed. ViewStub delays inflation until the view is actually needed.
Overdraw and GPU Profiling
What is Overdraw?
Overdraw occurs when the same pixel is drawn multiple times in a single frame. If you have a background, then a card on top, then text on top of the card, the GPU draws the pixel three times. Overdraw wastes GPU fill rate and can cause jank on lower-end devices.
Enabling Overdraw Visualization
On a physical device, enable it in Developer Options:
- Settings > Developer Options
- Debug GPU Overdraw > Show overdraw areas
The color coding shows overdraw levels:
- No color: No overdraw (1x)
- Blue: 1x overdraw (2x)
- Green: 2x overdraw (3x)
- Pink: 3x overdraw (4x)
- Red: 4x+ overdraw
Aim for mostly blue with no red areas.
Common Overdraw Sources
1. Default window background: Every Activity has a default background. Remove it if you draw your own:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Remove the default window background
window.setBackgroundDrawable(null)
setContentView(R.layout.activity_main)
}
}
Or in XML:
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
<item name="android:windowBackground">@android:color/transparent</item>
</style>
2. Overlapping opaque views: Stack views that fully overlap each other. If a card covers the entire screen, the views behind it are drawn unnecessarily.
3. Custom onDraw with background painting: If you override onDraw() and paint a background, then the system paints the view background on top, you get overdraw.
class CustomView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
init {
// Prevent the system from drawing the background twice
setWillNotDraw(false)
}
override fun onDraw(canvas: Canvas) {
// Draw your content directly
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
}
ClipRect and QuickReject
Canvas provides methods to skip drawing areas that are not visible:
override fun onDraw(canvas: Canvas) {
// Only draw the visible portion
canvas.clipRect(visibleArea)
// Draw content
drawContent(canvas)
}
canvas.quickReject() checks if a rect is entirely outside the clip bounds and can be skipped.
GPU Rendering Profiling
Enable GPU rendering profiling in Developer Options to see a real-time bar chart of frame rendering times:
- Settings > Developer Options > Profile GPU Rendering > On screen as bars
Each bar represents a frame. The green line is 16ms. Bars above the line mean dropped frames.
The bars are split into stages:
- Swap Buffers: Time to submit the frame to the GPU
- Issue Commands: Time to send draw commands
- Sync Upload: Time to upload textures
- Draw: Time to render
Using Systrace
Systrace gives a detailed timeline of what happened during each frame:
python systrace.py -t 5 -o trace.html gfx view
In Android Studio, use: Profile > Capture System Trace. Systrace shows:
- Which views are being measured and laid out
- When draw calls happen
- GPU and CPU usage per frame
- Where frames are dropped
Look for long bars in the UI thread row. These indicate frames that took too long.
RecyclerView and List Performance
RecyclerView is a Layout Problem
RecyclerView is the most common source of layout performance issues because it inflates and binds views on every scroll. Poorly optimized item layouts cause visible jank.
Optimize Item Layouts
RecyclerView item layouts should be as flat as possible:
<!-- GOOD: Flat item layout -->
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/thumbnail"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintLayout>
Avoid:
- Nested layouts inside items
layout_weightin LinearLayout items- Multiple
requestLayout()calls during bind
DiffUtil for Efficient Updates
Use DiffUtil instead of notifyDataSetChanged():
class ItemDiffCallback : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
}
// In your adapter
class MyAdapter : ListAdapter<Item, ViewHolder>(ItemDiffCallback()) {
// submitList triggers DiffUtil automatically
}
DiffUtil calculates the minimal set of changes and only updates the affected items, avoiding full list redraws.
View Binding in ViewHolder
Use View Binding instead of findViewById() for faster view access:
class ViewHolder(
private val binding: ItemLayoutBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
binding.title.text = item.title
binding.subtitle.text = item.subtitle
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding)
}
View Binding generates direct field references, eliminating the reflection cost of findViewById().
Prefetching and Caching
RecyclerView has built-in prefetching via GapWorker. It pre-binds the next item on the main thread while the current frame is being rendered:
recyclerView.layoutManager = LinearLayoutManager(context).apply {
initialPrefetchItemCount = 4
}
Set initialPrefetchItemCount to the number of visible items for optimal prefetching.
setHasFixedSize
If your RecyclerView item sizes do not change based on content, tell the system:
recyclerView.setHasFixedSize(true)
This skips the layout pass when items are added or removed, improving scroll performance.
Quiz
1. What is the target frame time for smooth 60fps rendering in Android?
2. Which layout attribute in LinearLayout causes a second measure pass?
3. What does the blue color indicate in the GPU overdraw visualization?
4. Why should you use setHasFixedSize(true) on a RecyclerView?
Flashcards
Question
What is the frame budget for smooth Android UI at 60fps?
Click to reveal answer
Answer
16.6ms per frame. If measure, layout, and draw exceed this, the frame drops and the user sees jank.
Question
What is overdraw in Android?
Click to reveal answer
Answer
When the same pixel is drawn multiple times in a single frame. It wastes GPU fill rate and causes jank on lower-end devices.
Question
Why is ConstraintLayout preferred over nested LinearLayout for performance?
Click to reveal answer
Answer
ConstraintLayout flattens the view hierarchy to a single level, reducing measure and layout passes from O(depth) to O(1) for most layouts.
Question
What does DiffUtil do in RecyclerView?
Click to reveal answer
Answer
It calculates the minimal set of changes between old and new lists and only updates affected items, avoiding full list redraws.
Revision Notes
Key Takeaways
- 1. Android targets 16.6ms per frame at 60fps
- 2. Deep view hierarchies increase measure and layout cost
- 3. ConstraintLayout flattens hierarchies to reduce nesting
- 4. Overdraw wastes GPU fill rate and causes jank
- 5. RecyclerView item layouts must be flat and use DiffUtil
Interview Tips
- • Explain the three phases of Android rendering: measure, layout, draw
- • Discuss why deep view hierarchies hurt performance
- • Describe how to detect and fix overdraw
- • Explain RecyclerView optimization techniques
- • Know how to use Layout Inspector and Systrace to diagnose issues
Cheat Sheet
Layout Performance Cheat Sheet
Frame budget: 16.6ms at 60fps
Three phases per frame: Measure, Layout, Draw
Hierarchy costs:
- Deep nesting = more measure/layout passes
- Use ConstraintLayout for flat hierarchies
- Avoid layout_weight in RecyclerView items
Overdraw:
- Enable: Developer Options > Debug GPU Overdraw
- Colors: Blue (1x), Green (2x), Pink (3x), Red (4x+)
- Fix: Remove window background, avoid overlapping opaque views
Tools:
- Layout Inspector: View hierarchy and render times
- GPU Rendering Profiler: Per-frame rendering bars
- Systrace: Detailed timeline of frame rendering
RecyclerView:
- Flat item layouts
- DiffUtil instead of notifyDataSetChanged()
- View Binding instead of findViewById()
- setHasFixedSize(true) when item sizes are constant
- initialPrefetchItemCount for prefetching