Kodokon kodokon.com

Performance: const, Rebuilds, Keys und DevTools

Beseitige unnötige Rebuilds dank const, Keys und Reconciliation und miss dann mit DevTools.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Flutter arbeitet mit drei Bäumen: den Widgets (unveränderliche Wegwerf-Konfigurationen), den Elements (lebenden Instanzen, die den Zustand tragen) und den Render Objects (Layout, Zeichnen). Ein Widget neu zu bauen ist billig; teuer sind das Layout und das kaskadierende Zeichnen. Bei einem Rebuild wendet Element.updateChild drei Regeln der Reihe nach an: Ist das neue Widget identisch (dieselbe Instanz) mit dem alten, wird der gesamte Teilbaum übersprungen; ist Widget.canUpdate wahr (gleicher runtimeType, gleicher key), bleibt das Element erhalten und wird einfach neu konfiguriert; andernfalls wird das Element zerstört und neu aufgebaut, inklusive Zustand. Das Schlüsselwort const nutzt die erste Regel aus: Ein konstanter Ausdruck wird vom Compiler kanonisiert, bei jedem Build kommt dieselbe Instanz zurück, identical ist wahr - und der ganze Zweig wird übersprungen.

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');
  }
}
Das Log erscheint nur einmal: const überspringt den Teilbaum.

Bei Listen von Kindern paart die Reconciliation zuerst nach Position. Ordne zustandsbehaftete Einträge ohne Keys um, und jedes Element behält den Zustand des vorherigen Bewohners seiner Position: Häkchen an der falschen Stelle, vertauschte Textfelder. Ein Key ändert die Regel für die Paarung: Der Algorithmus findet das Element mit demselben Key, auch wenn es innerhalb der Liste verschoben wurde. Verwende ValueKey mit einem stabilen fachlichen Bezeichner. GlobalKey geht weiter - damit kannst du einen ganzen Teilbaum umhängen, ohne seinen Zustand zu verlieren -, aber er hat echte Kosten (globales Register, Eindeutigkeit im gesamten Baum): Hebe ihn dir für echte Bedürfnisse auf, etwa Form oder das Umhängen von Teilbäumen.

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)),
          ),
      ],
    );
  }
}
Ohne stabilen Key wirft Dismissible einen Fehler, sobald du löschst.

Miss, bevor du optimierst. In den DevTools zeichnet die Ansicht Performance die Zeit pro Frame für die beiden kritischen Threads auf: UI (dein Dart-Code: Build, Layout) und Raster (das Rastern der Layer). Das Budget liegt bei etwa 16 ms bei 60 Hz. Ruckler auf der Raster-Seite bekommst du nicht in den Griff, indem du deine Builds optimierst: Sie kommen von den Kosten des Zeichnens (Schatten, saveLayer, überdimensionierte Bilder). Aktiviere Track widget builds, um zu sehen, wer neu baut, und debugRepaintRainbowEnabled, um die neu gezeichneten Zonen sichtbar zu machen - der Rahmen wechselt bei jedem Repaint die Farbe. RepaintBoundary isoliert einen Teilbaum in seinem eigenen Layer: Ein Spinner, der ununterbrochen neu zeichnet, macht seine Nachbarn nicht mehr schmutzig.

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: das Diagnose-Duo.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum wird ein const-Widget übersprungen, wenn sein Elternteil neu gebaut wird?
    • Die Instanz ist kanonisch: identical ist wahr, und Element.updateChild überspringt den gesamten Teilbaum
    • Der Compiler entfernt das Widget im Release aus dem Baum
    • const-Widgets haben keine build-Methode
  2. Du sortierst eine Liste von zustandsbehafteten Widgets ohne Keys um. Was passiert?
    • Die Elements werden nach Position gepaart: Jeder Eintrag erbt den Zustand des vorherigen Bewohners seiner Position
    • Flutter wirft im Debug-Modus eine Exception
    • Der Zustand folgt jedem verschobenen Widget automatisch
    • Die Liste weigert sich, neu zu bauen
  3. Was messen die beiden Diagramme des Performance-Overlays?
    • Die Zeit pro Frame des UI-Threads (Dart-Code) und des Raster-Threads (Rendern der Layer)
    • Den belegten Speicher und die Anzahl der Widgets
    • Die Zeit für den Build und die Zeit für die Garbage Collection