Profiling and performance

Flutter · Expert · 16 min

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 const constructors (identical const widgets are skipped), and context.select / ValueListenableBuilder for narrow rebuilds.
  • Keep build cheap; 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 shrinkWrap and intrinsic sizing (IntrinsicHeight), which force extra layout passes.

Raster-thread fixes:

  • Avoid saveLayer triggers: Opacity on large subtrees (use FadeTransition or color alpha instead), ShaderMask, ColorFilter, and clipping with anti-aliasing on animated content.
  • Wrap frequently repainting areas (a spinner, a video) in RepaintBoundary so 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 > Performance

Remember

  • 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.

Check yourself

  1. DevTools shows slow frames with the raster thread over budget but the UI thread fast. What's the most likely culprit?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/perf/best-practices