What frame rate in a Flutter app is good?
#flutter#performance#fps#frame-rate#optimization
Answer
Overview
Flutter targets 60 fps by default and 120 fps on high-refresh-rate displays. Each frame has a strict time budget — if your code exceeds it, the frame is dropped (jank).
Frame Rate Targets
| Display Refresh Rate | Target FPS | Frame Budget |
|---|---|---|
| 60 Hz | 60 fps | 16.67 ms per frame |
| 90 Hz | 90 fps | 11.11 ms per frame |
| 120 Hz | 120 fps | 8.33 ms per frame |
Flutter automatically adapts to the device's refresh rate.
What Is Good vs Bad Frame Rate
| Frame Rate | Quality | User Perception |
|---|---|---|
| 60 fps (stable) | Excellent | Smooth, fluid animations |
| 50-59 fps | Acceptable | Occasional minor stutters |
| 30-49 fps | Poor | Noticeable jank, jerky animations |
| Below 30 fps | Terrible | App feels broken, unusable |
A stable 60fps is more important than occasional spikes to 120fps. Consistency matters more than peak frame rate.
Two Threads, One Budget
Both threads must finish within the frame budget:
| Thread | Responsibility | If Overloaded |
|---|---|---|
| UI Thread | Dart code, widget tree, layout, build | Too much computation in text text |
| Raster Thread | GPU rendering via Skia/Impeller, compositing | Complex scenes, large images, shader compilation |
textFrame Budget (60fps): 16.67ms ├── UI Thread: build + layout + paint → must finish in ~16ms └── Raster Thread: rasterize to GPU → must finish in ~16ms If EITHER exceeds budget → frame dropped → jank!
How to Check Frame Rate
1. Performance Overlay (Built-in)
dartMaterialApp( showPerformanceOverlay: true, // Enable overlay home: const MyApp(), );
Or press
text
Ptext
flutter run- Top graph = Raster thread (GPU)
- Bottom graph = UI thread (Dart code)
- Green bars = within budget
- Red bars = exceeded budget (jank!)
- White horizontal line = 16ms marker
2. DevTools Performance Tab
bashflutter run --profile # Always profile on real device!
Open DevTools > Performance tab > Record a session > Inspect individual frame timings.
3. Monitor Programmatically
dartimport 'package:flutter/scheduler.dart'; void monitorFrameRate() { SchedulerBinding.instance.addTimingsCallback(( List<FrameTiming> timings, ) { for (final timing in timings) { final buildMs = timing.buildDuration.inMilliseconds; final rasterMs = timing.rasterDuration.inMilliseconds; final totalMs = timing.totalSpan.inMilliseconds; if (totalMs > 16) { debugPrint( 'Dropped frame! ' 'Build: ${buildMs}ms, ' 'Raster: ${rasterMs}ms, ' 'Total: ${totalMs}ms', ); } } }); }
Important: Always measure in profile mode on a physical device. Debug mode and emulators give inaccurate results.
What Causes Frame Drops
| Cause | Thread Affected | Solution |
|---|---|---|
| Heavy JSON parsing / sorting | UI | Use text text |
| Excessive widget rebuilds | UI | Use text text |
| Large image decoding | UI | Use text |
| Complex widget trees | Both | Simplify, use text |
| Shader compilation (first run) | Raster | Use Impeller or SkSL warm-up |
text | Raster | Use text |
| Synchronous I/O on main thread | UI | Use async methods |
Quick Tips for Maintaining 60fps
dart// ✅ Use const constructors const SizedBox(height: 16); const Text('Static text'); // ✅ Use ListView.builder for long lists ListView.builder( itemCount: 1000, itemBuilder: (context, i) => ListTile(title: Text('Item $i')), ); // ✅ Offload heavy work to Isolates final result = await compute(parseJson, rawData); // ✅ Wrap animated widgets RepaintBoundary( child: MyAnimatedWidget(), );
Goal: Keep every frame under 16ms on both UI and Raster threads. A stable 60fps = smooth, professional app.
Learn more at Flutter Performance Profiling.