| name | overdraw-and-layout |
| description | Reduce overdraw, layout depth, and unnecessary recomposition/re-render in native Android, iOS, Compose, SwiftUI, Flutter, and React Native UIs. |
Overdraw and Layout
Instructions
Overdraw means the GPU painted the same pixel multiple times. Deep layout hierarchies mean the CPU measured and positioned views the user never sees. Both waste the frame budget. This skill covers how to see the problem and how to fix it.
1. Visualize Overdraw
-
Android: Developer Options → Debug GPU overdraw → Show overdraw areas. Blue = 1x (fine), green = 2x, pink = 3x, red = 4x+. Red areas are the target.
-
iOS: Xcode → Debug → View Debugging → Rendering → Color Blended Layers. Green layers are opaque (good), red are blended (expensive). Also Color Misaligned Images and Color Off-screen Rendered.
-
Flutter: in main:
debugPaintLayerBordersEnabled = true; // shows layer boundaries
debugRepaintRainbowEnabled = true; // rainbow cycles on every repaint
-
React Native: with Flipper → Layout Inspector, enable Show Overdraw on Android device.
2. Fix Overdraw
- Set a single root background color. Remove nested background drawables that repaint the same area.
- Use opaque backgrounds for full-width rows. Blended (translucent) backgrounds force compositing.
- Replace shadow-under-image patterns with a pre-baked PNG or a CAGradientLayer clipped to the corners.
- iOS: set
isOpaque = true on views whose background is fully opaque.
- Android: remove
android:background from intermediate ViewGroups; let the window background show through.
- Flutter: avoid stacked
Container(decoration: BoxDecoration(color: ...)) for no reason.
3. Shrink Layout Depth
A deep hierarchy costs measure + layout passes on every frame.
Android View system:
- Prefer
ConstraintLayout over nested LinearLayout/RelativeLayout.
- Replace
ScrollView > LinearLayout > N children with RecyclerView.
- Use
<merge> in reusable layouts to collapse a pointless root.
Compose:
- Avoid nesting
Box inside Box inside Box. Use Modifier.padding/background/clickable instead of wrapper Box.
- Prefer
LazyColumn/LazyRow over Column { items.forEach { ... } }.
SwiftUI:
LazyVStack instead of VStack for large lists.
- Flatten
Group { Group { ... } } wrappers; they do not add value.
- Avoid
AnyView — it erases types and blocks SwiftUI's diffing.
Flutter:
- Replace
Column(children: [Padding(Padding(Padding(...)))]) with a single Padding + direct child.
- Use
Flex/Spacer/Expanded instead of multiple SizedBox.
- Use
RepaintBoundary around parts that repaint independently (e.g., a ticking clock above a static feed).
React Native:
- Collapse wrapper
<View> layers. Each native View adds a shadow node and a native view.
- Use
collapsable={true} hints and Fabric's view flattening.
flex: 1 everywhere can force extra layout passes; set explicit dimensions when known.
4. Minimize Recomposition / Re-render
Compose — only recompose what changed:
@Composable
fun Counter(count: Int) {
val label = remember(count) { "Count: $count" }
Text(label)
}
@Composable
fun Screen(state: ScreenState) {
val canSubmit by remember(state) { derivedStateOf { state.name.isNotBlank() } }
Button(enabled = canSubmit, onClick = state.onSubmit) { Text("Submit") }
}
Mark classes @Stable / @Immutable when their equality is value-based so Compose can skip recomposition.
SwiftUI — extract subviews so @State changes do not invalidate siblings. Use EquatableView for heavy leaf views.
React (RN) — React.memo rows; keep onPress identity stable with useCallback:
const Row = React.memo(function Row({ item, onPress }: Props) {
return <Pressable onPress={() => onPress(item.id)}><Text>{item.title}</Text></Pressable>;
});
Flutter — prefer const constructors everywhere possible. Use Selector (provider) or select (Riverpod) to subscribe to a slice of state instead of the whole object.
5. Lists at Scale
- Android: use
RecyclerView with DiffUtil or ListAdapter. Set setHasFixedSize(true) if bounds do not change with content.
- Compose:
LazyColumn with stable key = { it.id } and contentType when heterogeneous.
- SwiftUI:
List or LazyVStack(pinnedViews:). Provide id: explicitly; avoid UUID() per render.
- Flutter:
ListView.builder with itemExtent/prototypeItem for uniform rows.
- RN:
FlatList with keyExtractor, getItemLayout, removeClippedSubviews={true}, or migrate to FlashList.
6. Verify with Layout Profilers
- Android Studio → Layout Inspector → shows actual view hierarchy depth and recomposition counts in Compose.
- Xcode → Debug View Hierarchy → see layer counts per screen.
- Flutter DevTools → Widget Inspector → check rebuild counts per widget.
- React DevTools Profiler → commit-by-commit render durations.
Checklist