Kodokon kodokon.com

Performance: const, rebuilds, keys and DevTools

Eliminate useless rebuilds thanks to const, keys and reconciliation, then measure with DevTools.

10 min · 3 questions

Open this lesson in Kodokon

Flutter manipulates three trees: the widgets (immutable, disposable configurations), the Elements (living instances that carry state) and the render objects (layout, painting). Rebuilding a widget is cheap; what costs is layout and cascading paint. During a rebuild, Element.updateChild applies three rules in order: if the new widget is identical (same instance) to the old one, the entire subtree is short-circuited; if Widget.canUpdate is true (same runtimeType, same key), the Element is kept and simply reconfigured; otherwise, the Element is destroyed then re-inflated, state included. The const keyword exploits the first rule: a constant expression is canonicalized by the compiler, the same instance comes back on every build, identical is true - and the whole branch is skipped.

DART
import 'package:flutter/material.dart';

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() =>
      _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const _Header(),
        Text('Total: $_count'),
        FilledButton(
          onPressed: () => setState(() => _count++),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    debugPrint('build _Header');
    return const Text('Counter');
  }
}
The log appears only once: const skips the subtree.

For lists of children, reconciliation pairs first by position. Reorder stateful items without keys and each Element keeps the state of the previous occupant of its position: checkboxes ticked in the wrong place, text fields swapped. A Key changes the pairing rule: the algorithm finds the Element carrying the same key, even when moved within the list. Use ValueKey on a stable business identifier. GlobalKey goes further - it lets you reparent an entire subtree without losing its state - but it has a real cost (global registry, uniqueness constraint across the whole tree): reserve it for genuine needs like Form or reparenting.

DART
import 'package:flutter/material.dart';

class TodoList extends StatelessWidget {
  const TodoList({super.key, required this.items});

  final List<String> items;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        for (final item in items)
          Dismissible(
            key: ValueKey(item),
            onDismissed: (direction) {},
            child: ListTile(title: Text(item)),
          ),
      ],
    );
  }
}
Without a stable key, Dismissible throws an error as soon as you delete.

Measure before optimizing. In DevTools, the Performance view traces per-frame time for the two critical threads: UI (your Dart code: build, layout) and raster (rasterizing the layers). The budget is about 16 ms at 60 Hz. Jank on the raster side isn't fixed by optimizing your builds: it comes from the cost of painting (shadows, saveLayer, oversized images). Enable Track widget builds to see who's rebuilding, and debugRepaintRainbowEnabled to visualize repainted zones - the border changes color on every repaint. RepaintBoundary isolates a subtree into its own layer: a spinner repainting continuously no longer dirties its neighbors.

DART
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  debugRepaintRainbowEnabled = true;
  runApp(const DemoApp());
}

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      showPerformanceOverlay: true,
      home: Scaffold(
        body: Center(
          child: RepaintBoundary(
            child: CircularProgressIndicator(),
          ),
        ),
      ),
    );
  }
}
Frame overlay + repaint rainbow: the diagnostics duo.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Why is a const widget ignored when its parent rebuilds?
    • The instance is canonical: identical is true, and Element.updateChild short-circuits the entire subtree
    • The compiler removes the widget from the tree in release
    • const widgets have no build method
  2. You reorder a list of stateful widgets without keys. What happens?
    • The Elements are paired by position: each item inherits the state of the previous occupant of its position
    • Flutter throws an exception in debug mode
    • State automatically follows each moved widget
    • The list refuses to rebuild
  3. What do the two graphs of the performance overlay measure?
    • Per-frame time of the UI thread (Dart code) and the raster thread (layer rendering)
    • Allocated memory and the number of widgets
    • Build time and garbage-collection time