Skip to content
Boring Doc
Boring Doc explanation Verified against Flutter 3.47.7 4 min readperformancejankbuildrebuildconst

Performance

Jank, rebuilds, raster cost and how to measure.

Flutter is fast by default; jank comes from doing more work per frame than the ~16 ms budget allows. Keep build() cheap and scoped (const widgets, small StatefulWidgets, lazy lists), avoid saveLayer-triggering effects such as Opacity and clipping in animations, and measure in profile mode on a real device with the DevTools Performance view before changing anything.

  1. A frame has two halves on two threads: the UI thread runs your Dart (animate, build, layout, paint, composite) and the raster thread turns the layer tree into pixels. Either half taking too long drops the frame.
  2. setState marks that State's element dirty and everything below it rebuilds unless a child is the identical widget instance. Keep setState low in the tree and split big build methods into small widgets.
  3. const widgets are the identical instance across builds, so the element tree stops descending into them. Prefer const constructors and StatelessWidget classes over helper functions that return widgets.
  4. Never create long lists eagerly (ListView(children: [...]) or a Column with thousands of children); ListView.builder builds only what is visible, and itemExtent/prototypeItem let the scroll machinery skip layout work.
  5. Opacity, ShaderMask, ColorFilter and Clip.antiAliasWithSaveLayer can call saveLayer, which allocates an offscreen buffer and is expensive on mobile GPUs. Fade with AnimatedOpacity/FadeTransition or a semi-transparent colour instead.
  6. RepaintBoundary isolates a subtree into its own layer so a static, expensive picture is not repainted when a sibling animates. It costs memory, so add it where profiling shows repaint, not everywhere.
  7. Debug mode is not representative (JIT, asserts). Measure with flutter run --profile on the slowest device you support, and read the Performance overlay or DevTools frame chart.
Flow diagramThe frame pipeline

Everything inside the UI-thread box is your Dart code plus the framework; the raster thread cannot be touched directly but is slowed by what the UI thread asks it to draw (saveLayer, clips, shadows, large images).

A kitchen with a 16 ms order window

Every frame is an order that must leave the kitchen in about 16 ms. The UI thread is the prep cook: it reads the ticket (animation values), decides what changed (build), measures portions (layout) and writes the plating instructions (paint). The raster thread is the line cook who actually plates from those instructions. Rebuilding the whole menu when one garnish changed (setState high in the tree), chopping every vegetable in the walk-in instead of only tonight's (eager lists), or asking for a special plate that needs its own station (saveLayer from Opacity) are all ways to miss the window. A RepaintBoundary is a pre-plated side kept under the lamp: reused as long as it does not change.

Where the analogy breaks: Kitchens can add cooks; Flutter's UI thread is single-threaded Dart, so parallelism only helps for pure computation moved to an isolate. The analogy also hides that the raster side is on a different clock: a frame can be built in 2 ms and still jank because the GPU work it produced was heavy, which is why both graphs in the performance overlay matter.

Minimal working example

A lazy list with const leaves and a fixed item extent
dart
import 'package:flutter/material.dart';

class MessageList extends StatelessWidget {
  const MessageList({super.key, required this.messages});

  final List<String> messages;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: messages.length,
      prototypeItem: const ListTile(title: Text('prototype')),
      itemBuilder: (context, index) {
        return ListTile(
          leading: const Icon(Icons.message),
          title: Text(messages[index]),
        );
      },
    );
  }
}
Expected: Scrolling 10,000 messages stays smooth because only the visible rows plus a small cache window exist at any moment.Flutter ≥ 3.0.0Work with long lists (cookbook)
  1. L4A const constructor lets a parent write const MessageList(...) when the input is constant, and lets Flutter skip this subtree on rebuild.
  2. L10ListView.builder creates items on demand as they scroll into view, instead of building all of them up front.
  3. L12prototypeItem (or itemExtent) tells the viewport every row's height, so it can jump and estimate scroll extent without laying out children.
  4. L15A const leaf is the same instance every build; the element diff short-circuits on it.

Use cases

  • Scrolling lists or grids that stutter: switch to builder constructors, set an item extent, and check that rows are not doing I/O or decoding in build.
  • Animations that drop frames: check for Opacity, clips or shadows inside the animated subtree, pass static children through AnimatedBuilder, and consider a RepaintBoundary around static neighbours.
  • A screen that rebuilds too much: use the DevTools rebuild counts or the inspector's 'track widget builds' to find the setState that is too high in the tree.
  • Any time before shipping: run in profile mode on the slowest supported device and watch the performance overlay for red bars.

Benefits

  • Most fixes are structural (split a widget, add const, use a builder) and make code clearer as well as faster.
  • Flutter's tooling shows per-frame timing for both threads, so you can tell a Dart problem (UI graph) from a GPU problem (raster graph) in seconds.

Limitations

  • Micro-optimising before measuring usually targets the wrong thing; the framework already skips unchanged elements and render objects.
  • RepaintBoundary and raster caching trade GPU memory for time; sprinkled everywhere they can make things slower.
  • Overriding operator == on widgets to avoid rebuilds is a known anti-pattern that leads to O(n^2) comparisons and defeats compiler optimisations.
  • Shader compilation jank on first run is an engine concern (Impeller precompiles); it is not fixed by widget-level changes.

Alternatives and trade-offs

  • Isolates (compute, Isolate.run) — The slow part is pure CPU work such as parsing a large JSON payload; move it off the UI thread.
  • Slivers and CustomScrollView — A single ListView.builder is not enough: mixed headers, grids and lists in one scroll view.
  • Precaching and placeholders (precacheImage, FadeInImage) — Perceived slowness is image loading rather than frame time.

Building a long list eagerly

Problem: ListView(children: items.map(...).toList()) or a Column inside a SingleChildScrollView builds and lays out every item at once, even those far off screen. With thousands of rows the first frame and every rebuild are slow.

Fix: Use ListView.builder (or GridView.builder, ListView.separated) so items are created lazily, and give the viewport an itemExtent or prototypeItem when rows have a fixed height.

✗ All 10,000 rows built up front
dart
ListView(
  children: [for (final m in messages) ListTile(title: Text(m))],
)
✓ Rows built on demand
dart
ListView.builder(
  itemCount: messages.length,
  itemExtent: 56,
  itemBuilder: (context, i) => ListTile(title: Text(messages[i])),
)

Animating with the Opacity widget

Problem: Opacity with a value between 0 and 1 wraps its child in saveLayer. Changing the value every frame means an offscreen buffer and a render-target switch per frame, which is a known cause of raster-thread jank on mobile.

Fix: Use AnimatedOpacity or FadeTransition, which animate the opacity layer without rebuilding, use FadeInImage for images, or draw simple shapes and text with a semi-transparent colour.

✗ Opacity driven by a controller
dart
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) =>
      Opacity(opacity: _controller.value, child: child),
  child: const Logo(),
)
✓ FadeTransition
dart
FadeTransition(
  opacity: _controller,
  child: const Logo(),
)

Calling setState at the top of the screen

Problem: A page-level State that calls setState for a counter, a timer or a text field rebuilds the whole page, including app bars, lists and images that did not change.

Fix: Move the changing piece into its own small StatefulWidget (or a ValueListenableBuilder / ListenableBuilder), and make the unchanged siblings const so the rebuild stops at them.

Measuring in debug mode on an emulator

Problem: Debug builds run JIT-compiled Dart with asserts and extra checks, and emulators do not have phone GPUs. Jank seen there often disappears in release, and real jank on low-end phones is invisible.

Fix: Profile with flutter run --profile on a physical device, ideally the slowest one you support, and use the Performance overlay or DevTools Performance view to confirm which thread is over budget.

This page is Boring Doc’s independent explanation. For the complete specification use the official pages below (content © The Flutter Authors, CC BY 3.0; code samples BSD). Dates show when the explanation was last checked and when the official page last changed.

  • Performance (overview) Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/perf
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Performance best practices Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/perf/best-practices
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Flutter performance profiling Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/perf/ui-performance
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Improving rendering performance Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/perf/rendering-performance
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Work with long lists Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/cookbook/lists/long-lists
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • DevTools (learning pathway) Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/learn/pathway/tutorial/devtools
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • https://api.flutter.dev/flutter/widgets/Opacity-class.html
    Last checked
    Oct 21, 2026
Was this useful?
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.