Smooth 60–120 fps scrolling and animation is what users notice, and fixes require measuring first.
Always measure in profile mode (flutter run --profile) on a real, preferably low-end, device. Debug mode is much slower and misleading.
DevTools tools:
- Performance view — frame chart split into UI (build/layout/paint in Dart) and raster (GPU) time. Red frames exceeded the budget.
- CPU profiler — which Dart functions consume time.
- Track widget rebuilds and "Highlight repaints" to spot unnecessary work.
- Memory view — leaks and large allocations.
UI-thread fixes:
- Rebuild less: push state down, split big widgets, use
constconstructors (identical const widgets are skipped), andcontext.select/ValueListenableBuilderfor narrow rebuilds. - Keep
buildcheap; never parse JSON, sort large lists or do I/O in it. Move heavy CPU work to an isolate. - Use lazy builders for lists and grids; avoid
shrinkWrapand intrinsic sizing (IntrinsicHeight), which force extra layout passes.
Raster-thread fixes:
- Avoid
saveLayertriggers:Opacityon large subtrees (useFadeTransitionor color alpha instead),ShaderMask,ColorFilter, and clipping with anti-aliasing on animated content. - Wrap frequently repainting areas (a spinner, a video) in
RepaintBoundaryso the rest isn't repainted. - Size images to display size with
cacheWidth.
Check app size with flutter build apk --analyze-size and startup with --trace-startup.
Example
import 'package:flutter/material.dart';
// Before: the whole page rebuilds every tick and a static header is rebuilt too.
// After: only the ticking text rebuilds; static parts are const; the spinner is isolated.
class Dashboard extends StatelessWidget {
const Dashboard({super.key, required this.seconds});
final ValueNotifier<int> seconds;
@override
Widget build(BuildContext context) => Column(children: [
const _ExpensiveHeader(), // const: skipped on rebuild
ValueListenableBuilder<int>( // narrow rebuild scope
valueListenable: seconds,
builder: (context, s, _) => Text('Uptime: ${s}s'),
),
const RepaintBoundary(child: CircularProgressIndicator()), // repaints alone
]);
}
class _ExpensiveHeader extends StatelessWidget {
const _ExpensiveHeader();
@override
Widget build(BuildContext context) => const Text('Server dashboard');
}
// Measure: flutter run --profile, then DevTools > PerformanceRemember
- Profile in profile mode on real devices with DevTools
- Shrink rebuild scope; use const and narrow listeners
- Avoid saveLayer-heavy effects; isolate repaints with RepaintBoundary
Watch out for
- Optimizing blindly in debug mode without looking at the UI vs raster thread breakdown.