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 RateTarget FPSFrame Budget
60 Hz60 fps16.67 ms per frame
90 Hz90 fps11.11 ms per frame
120 Hz120 fps8.33 ms per frame

Flutter automatically adapts to the device's refresh rate.


What Is Good vs Bad Frame Rate

Frame RateQualityUser Perception
60 fps (stable)ExcellentSmooth, fluid animations
50-59 fpsAcceptableOccasional minor stutters
30-49 fpsPoorNoticeable jank, jerky animations
Below 30 fpsTerribleApp 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:

ThreadResponsibilityIf Overloaded
UI ThreadDart code, widget tree, layout, buildToo much computation in
text
build()
, heavy
text
setState()
Raster ThreadGPU rendering via Skia/Impeller, compositingComplex scenes, large images, shader compilation
text
Frame 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)

dart
MaterialApp(
  showPerformanceOverlay: true, // Enable overlay
  home: const MyApp(),
);

Or press

text
P
in the terminal during
text
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

bash
flutter run --profile  # Always profile on real device!

Open DevTools > Performance tab > Record a session > Inspect individual frame timings.

3. Monitor Programmatically

dart
import '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

CauseThread AffectedSolution
Heavy JSON parsing / sortingUIUse
text
compute()
or
text
Isolate.run()
Excessive widget rebuildsUIUse
text
const
, targeted
text
setState()
Large image decodingUIUse
text
precacheImage()
, resize images
Complex widget treesBothSimplify, use
text
RepaintBoundary
Shader compilation (first run)RasterUse Impeller or SkSL warm-up
text
Opacity
on large subtrees
RasterUse
text
AnimatedOpacity
instead
Synchronous I/O on main threadUIUse 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.