| name | frame-rate-optimization |
| description | Hit 60/90/120Hz frame rate budgets by balancing CPU (UI thread) and GPU (RenderThread/Metal) work. Use when frame time exceeds the device refresh budget. |
Frame Rate Optimization
Instructions
A smooth UI is not about doing less; it is about doing work within the frame budget and not blocking the pipeline. This skill covers the mental model and the fixes that apply across iOS, Android, Flutter, and React Native.
1. Know the Budget
| Refresh rate | Frame budget | Slack after 4ms overhead |
|---|
| 60 Hz | 16.67 ms | ~12 ms |
| 90 Hz | 11.11 ms | ~7 ms |
| 120 Hz | 8.33 ms | ~4 ms |
Work on two threads must each fit in the budget:
- UI / main thread — layout, measure, produce a display list.
- Render thread / GPU — rasterize, composite, present.
2. Pipeline by Platform
- Android:
Choreographer.doFrame → View.measure/layout/draw → RenderThread → SurfaceFlinger.
- iOS: Run loop →
CALayer commit → CARenderServer → GPU → display. Core Animation always runs on a dedicated thread.
- Flutter: Dart UI thread (widget → element → render object) → Skia/Impeller raster thread → GPU.
- React Native: JS thread → shadow thread (Yoga layout) → UI thread / Fabric → RenderThread.
If either thread exceeds the budget, a frame is dropped. Traces usually show which.
3. UI-Thread Wins
- Flatten view hierarchies. In Android avoid nested
LinearLayout; use ConstraintLayout or Compose. In iOS flatten UIStackView nesting beyond 3 levels.
- Cache expensive measurements. In SwiftUI avoid
GeometryReader in list rows; prefer fixedSize() or ViewThatFits. In Compose hoist remember { } for computed text styles.
- Avoid per-frame allocation. In Flutter use
const widgets. In RN memoize row components (React.memo, stable keys).
Compose example — stable lambdas:
@Composable
fun FeedRow(item: Item, onClick: (Item) -> Unit) {
Row(Modifier.clickable { onClick(item) }) { }
}
SwiftUI — equatable views:
struct Row: View, Equatable {
let item: Item
var body: some View { HStack { Text(item.title) } }
static func == (a: Row, b: Row) -> Bool { a.item.id == b.item.id }
}
List(items) { Row(item: $0).equatable() }
4. GPU / Raster-Thread Wins
- Avoid large blurs. A full-screen Gaussian blur on a 1080p phone costs ~4-6 ms on the raster thread. Replace with a static snapshot or a smaller blurred layer.
- Rasterize expensive static trees. Flutter:
RepaintBoundary. Android View: setLayerType(LAYER_TYPE_HARDWARE). iOS: layer.shouldRasterize = true; layer.rasterizationScale = UIScreen.main.scale for static content only.
- Prefer vector assets over large bitmaps for icons, but cap vector complexity. Extremely complex SVGs can beat large PNGs on CPU but cost more on GPU.
- Use platform-native gradients.
LinearGradient on GPU beats painting pixels in CPU.
5. Animations at High Refresh Rate
- Drive animations from the platform compositor, not JS/Dart setState loops.
- Flutter:
AnimatedBuilder with a Ticker; avoid setState per frame at root.
- RN:
Animated with useNativeDriver: true, or Reanimated worklets.
- Compose:
animate*AsState, Animatable, or rememberInfiniteTransition — they run on the choreographer, not recomposition.
- SwiftUI:
withAnimation and TimelineView; Core Animation implicit animations when possible.
RN Reanimated worklet:
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({ transform: [{ translateX: offset.value }] }));
offset.value = withSpring(100);
6. Target the Right Refresh Rate
- iOS: request 120Hz with
CADisplayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 80, maximum: 120, preferred: 120). Without this, ProMotion falls back to 60Hz for UIKit animations.
- Android: declare 120Hz support in manifest:
<meta-data android:name="android.max_aspect" .../> is not enough; set Window.attributes.preferredDisplayModeId or use Surface.setFrameRate() on Android 11+.
- Flutter:
GestureBinding.instance.resamplingEnabled = true helps touch-driven 120Hz.
7. Verify
adb shell dumpsys gfxinfo com.example.app framestats prints a histogram of frame times. Expect the 95th-percentile bucket under the budget.
Instruments Core Animation FPS gauge or UICoreAnimationFramesPerSecond metric from MetricKit can be wired into CI.
Flutter DevTools Performance shows UI and Raster thread stacks side by side for every frame.
Checklist