Skip to content
advanced Phase 11 · Performance & Optimization

Layout Performance

Optimize rendering: flatten hierarchies, use ConstraintLayout, reduce overdraw, and profile GPU.

45m
2 problems
Topic Progress 0%

Layout Performance Fundamentals

How Android Renders Frames

Android targets 60 frames per second (16.6ms per frame). Each frame goes through three phases:

  1. Measure: Each view determines its size based on parent constraints
  2. Layout: Each view positions itself within its parent
  3. 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:

  1. Settings > Developer Options
  2. 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:

  1. 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_weight in 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?

Question 1 options

2. Which layout attribute in LinearLayout causes a second measure pass?

Question 2 options

3. What does the blue color indicate in the GPU overdraw visualization?

Question 3 options

4. Why should you use setHasFixedSize(true) on a RecyclerView?

Question 4 options

Flashcards

Question

What is the frame budget for smooth Android UI at 60fps?

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?

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?

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?

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