Flutter

Flutter Performance-Optimierung: Vollständiger Leitfaden

14 Min. Lesezeit9. Februar 2026Aktualisiert 9. März 2026
Flutter performanceFlutter optimizasyonFlutter hızlandırmaFlutter widget rebuildFlutter memoryFlutter profilingFlutter DevToolsFlutter best practicesFlutter lag fix

Performance-Arbeit sollte immer datengetrieben sein. Zu raten, wo die Engpasse liegen, ist verschwendete Zeit. In diesem Leitfaden gehe ich die Techniken, Werkzeuge und Denkansatze durch, die eine ruckelnde Flutter-App in ein flussiges 60fps- (oder 120fps-) Erlebnis verwandeln.

Warum Performance wichtig ist

Nutzer merken es. Studien zeigen, dass eine Verzogerung von 100ms in der UI-Reaktion als trage wahrgenommen wird und alles uber 300ms das Gefuhl vermittelt, die App sei defekt. In Flutter bedeutet ein einzelner ausgelassener Frame, dass die Render-Pipeline ihr 16ms-Budget (bei 60Hz) uberschritten hat. Multipliziert man das mit Scrolling-Listen, Seitenubergangen und Animationen, fuhlt sich die App billig an, egal wie gut die Features sind.

Die 10 grossten Performance-Killer (und ihre Losungen)

1. Den gesamten Widget-Baum neu aufbauen

Das ist das haufigste Problem, das ich in Produktions-Codebasen sehe. Ein einzelner setState-Aufruf an der Spitze eines grossen Widget-Baums lost einen Rebuild aller Nachfolger aus.

Anti-Pattern:

dart
class MyHomePage extends StatefulWidget {
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // Dieses teure Widget wird bei jeder _counter-Anderung neu gebaut
          ExpensiveHeader(),
          ExpensiveProductList(),
          Text('Zahler: $_counter'),
          ElevatedButton(
            onPressed: () => setState(() => _counter++),
            child: Text('Erhohen'),
          ),
        ],
      ),
    );
  }
}

Losung: Den veranderlichen Teil in ein eigenes Widget auslagern.

dart
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          const ExpensiveHeader(),
          const ExpensiveProductList(),
          // Nur dieses kleine Widget wird neu gebaut
          CounterDisplay(),
        ],
      ),
    );
  }
}

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

  @override
  State<CounterDisplay> createState() => _CounterDisplayState();
}

class _CounterDisplayState extends State<CounterDisplay> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Zahler: $_counter'),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: Text('Erhohen'),
        ),
      ],
    );
  }
}

2. Fehlende const-Konstruktoren

Jedes Widget ohne const wird bei jedem Build als neue Instanz erstellt. Das Framework muss es dann gegen den vorherigen Baum abgleichen. Mit const weiss Flutter, dass sich das Widget nicht verandert hat, und uberspringt es vollstandig.

dart
// Schlecht - neue Instanz bei jedem Build
Container(
  padding: EdgeInsets.all(16),
  child: Text('Hallo'),
)

// Gut - Compile-Time-Konstante, wird beim Rebuild uebersprungen
const Padding(
  padding: EdgeInsets.all(16),
  child: Text('Hallo'),
)

3. Alle Listenelemente sofort erstellen

Die Verwendung von ListView(children: [...]) mit Hunderten von Elementen bedeutet, dass alle gleichzeitig erstellt und gelayoutet werden, auch die weit ausserhalb des Bildschirms.

dart
// Schlecht - erstellt alle 10.000 Elemente sofort
ListView(
  children: List.generate(10000, (i) => ProductCard(product: products[i])),
)

// Gut - erstellt nur sichtbare Elemente plus einen kleinen Puffer
ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, i) => ProductCard(product: products[i]),
)

4. Nicht optimierte Bilder

Ein 4000x3000-Pixel-Bild in ein 200x150-Widget zu laden, verschwendet enorme Mengen an Speicher und Dekodierungszeit.

dart
// Schlecht - volle Auflosung wird in den Speicher geladen
Image.network('https://example.com/grosses-foto.jpg')

// Gut - nur das dekodieren, was benotigt wird
Image.network(
  'https://example.com/grosses-foto.jpg',
  cacheWidth: 400,  // 2x Anzeigebreite fuer Retina
  cacheHeight: 300,
)

5. Schwere Berechnungen auf dem UI-Isolate

Das Parsen einer grossen JSON-Antwort, Bildverarbeitung oder Kryptografie-Operationen auf dem Haupt-Isolate blockieren die Render-Pipeline.

dart
// Schlecht - blockiert die UI
final data = jsonDecode(riesiqerJsonString);

// Gut - auf ein separates Isolate auslagern
final data = await compute(jsonDecode, riesiqerJsonString);

Fur komplexere Arbeit verwenden Sie Isolate.spawn oder das IsolatePool-Muster.

6. Ubermassiger Einsatz von Opacity und ColorFiltered

Das Opacity-Widget erstellt eine separate Compositing-Ebene. Verschachtelt oder auf grossen Unterbaumen eingesetzt, totet es die GPU-Performance.

dart
// Schlecht - erzwingt Offscreen-Buffer fur gesamten Unterbaum
Opacity(
  opacity: 0.5,
  child: GrossesKomplexesWidget(),
)

// Besser - wenn nur Text-/Icon-Transparenz benotigt wird
Text('Hallo', style: TextStyle(color: Colors.black.withOpacity(0.5)))

7. RepaintBoundary nicht verwenden

Ohne Repaint-Grenzen kann eine kleine Animation dazu fuhren, dass der gesamte Bildschirm in jedem Frame neu gezeichnet wird.

dart
// Haeufig wechselnde Widgets umschliessen
RepaintBoundary(
  child: AnimatedProgressIndicator(),
)

8. Teure build()-Methoden

Filtern, Sortieren oder Berechnungen innerhalb von build() bedeutet, dass es bei jedem einzelnen Rebuild ausgefuhrt wird.

dart
// Schlecht - sortiert bei jedem Build
@override
Widget build(BuildContext context) {
  final sorted = List.of(items)..sort((a, b) => a.date.compareTo(b.date));
  return ListView.builder(
    itemCount: sorted.length,
    itemBuilder: (ctx, i) => ItemTile(sorted[i]),
  );
}

// Gut - bei Datenanderung sortieren, Ergebnis cachen
late List<Item> _sortedItems;

void _updateItems(List<Item> items) {
  _sortedItems = List.of(items)..sort((a, b) => a.date.compareTo(b.date));
  setState(() {});
}

9. Shader-Kompilierungs-Ruckler ignorieren

Wenn Flutter zum ersten Mal auf einen neuen Shader trifft, kompiliert es ihn spontan, was ein sichtbares Stocken verursacht. Dies ist besonders beim ersten Start merkbar.

Losung: Verwenden Sie flutter run --profile --cache-sksl --purge-persistent-cache wahrend eines Aufwarmdurchlaufs, dann bundeln Sie die erfassten Shader mit --bundle-sksl-path.

10. Unbeschrankte Intrinsic-Berechnungen

IntrinsicHeight und IntrinsicWidth sind im schlimmsten Fall O(n^2), weil sie einen spekulativen Layout-Durchlauf machen. Vermeiden Sie sie in Listen.

dart
// Schlecht innerhalb einer ListView
IntrinsicHeight(
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [LeftPanel(), RightPanel()],
  ),
)

// Besser - feste Hohe oder LayoutBuilder verwenden
SizedBox(
  height: 120,
  child: Row(
    children: [LeftPanel(), RightPanel()],
  ),
)

DevTools-Profiling: Schritt fur Schritt

Flutter DevTools ist Ihr bester Verbundeter. Hier ist der praktische Workflow, den ich bei jeder Performance-Untersuchung befolge.

Schritt 1: Im Profile-Modus starten

bash
flutter run --profile

Profilieren Sie niemals im Debug-Modus. Der Debug-Modus deaktiviert alle Optimierungen und liefert irrefuhrende Zahlen. Der Profile-Modus verwendet AOT-Kompilierung wie der Release-Modus, halt aber die Observatory- und DevTools-Verbindung offen.

Schritt 2: DevTools offnen

Drucken Sie im Terminal v, um DevTools im Browser zu offnen, oder starten Sie es uber das Flutter-Panel Ihrer IDE.

Schritt 3: Performance-Overlay

Aktivieren Sie zunachst das Performance-Overlay. Es zeigt zwei Graphen:

  • UI-Thread (oben): Zeit fur Widget-Aufbau und Dart-Code-Ausfuhrung
  • Raster-Thread (unten): Zeit fur Compositing und Zeichnen auf der GPU

Wird der UI-Balken rot, ist Ihr Dart-Code zu langsam. Wird der Raster-Balken rot, haben Sie zu viele Ebenen oder teure Zeichenoperationen.

Schritt 4: Timeline-Events

Wechseln Sie zum Performance-Tab. Zeichnen Sie eine problematische Interaktion auf (Scrollen, Seitenubergang, Animation). Dann untersuchen Sie das Flame-Chart:

  • Suchen Sie nach langen build-Phasen. Sie deuten auf teure Widget-Baume hin.
  • Suchen Sie nach langen layout-Phasen. Sie deuten auf komplexe Layout-Constraints oder Intrinsic-Berechnungen hin.
  • Suchen Sie nach paint-Spitzen. Sie zeigen fehlende Repaint-Grenzen an.

Schritt 5: Widget-Rebuild-Zahler

Aktivieren Sie in DevTools "Track widget rebuilds". Dies blendet einen Zahler uber jedem Widget ein, der anzeigt, wie oft es wahrend Ihrer Interaktion neu gebaut wurde. Jedes Widget, das in einer statischen Ansicht mehr als einmal neu gebaut wird, ist verdachtig.

Schritt 6: Iterieren

Beheben Sie den grossten Ubeltater, profilieren Sie erneut, bestatigen Sie die Verbesserung und wiederholen Sie. Widerstehen Sie dem Drang, alles auf einmal zu beheben. Gezielte, gemessene Anderungen sind weitaus effektiver.

Speicherleck-Erkennung und -Vermeidung

Speicherlecks in Flutter sind subtil. Der Garbage Collector kummert sich um das meiste, aber bestimmte Muster verhindern, dass Objekte freigegeben werden.

Haufige Leck-Quellen

Vergessene Listener und Subscriptions:

dart
class _MyWidgetState extends State<MyWidget> {
  late StreamSubscription _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = someStream.listen((data) {
      setState(() { /* aktualisieren */ });
    });
  }

  // Wenn Sie das vergessen, halt die Subscription eine Referenz
  // auf dieses State-Objekt fuer immer
  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}

Nicht aufgeraumte Animation-Controller:

dart
class _AnimatedWidgetState extends State<AnimatedWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  void dispose() {
    _controller.dispose(); // Kritisch!
    super.dispose();
  }
}

Closures, die BuildContext einfangen:

dart
// Gefahrlich - die Closure fangt den Context ein, der den Element-Baum halt
void _onTap(BuildContext context) {
  Future.delayed(Duration(seconds: 5), () {
    // Wenn das Widget wahrend der 5s disposed wird, ist dieser Context ungueltig
    Navigator.of(context).push(...);
  });
}

// Sicherer - mounted pruefen
void _onTap(BuildContext context) {
  Future.delayed(Duration(seconds: 5), () {
    if (!mounted) return;
    Navigator.of(context).push(...);
  });
}

Den Memory-Profiler verwenden

  1. Offnen Sie DevTools und gehen Sie zum Memory-Tab.
  2. Machen Sie einen Snapshot vor der Interaktion.
  3. Fuhren Sie die Aktion aus (z.B. zu einem Bildschirm navigieren und zuruck, 5-mal).
  4. Machen Sie einen weiteren Snapshot.
  5. Verwenden Sie die Diff-Ansicht, um gewachsene Objekte zu sehen. Wenn das Navigieren zu einem Bildschirm und zuruck jedes Mal neue Objekte erstellt, ohne die alten freizugeben, haben Sie ein Leck.

Achten Sie besonders auf wachsende Zahlen von State-Objekten, Stream-Controllern und Animation-Controllern.

Praxisbeispiel: Echte Performance-Gewinne

In einer Produktions-E-Commerce-App, die ich optimiert habe, fiel der Produktlisten-Bildschirm beim schnellen Scrollen auf 20fps. Die Ursachen waren:

  • Jede ProductCard hatte einen Opacity-Wrapper fur einen dezenten Fade-Effekt, wodurch uber 50 Compositing-Ebenen auf dem Bildschirm entstanden.
  • Produktbilder wurden in voller Auflosung (2000px breit) fur 180px breite Thumbnails geladen.
  • Ein BlocBuilder auf Scaffold-Ebene baute die gesamte Seite bei jedem Warenkorb-Update neu auf.

Nach gezielten Korrekturen (Ersetzen von Opacity durch vorberechnete Farben, Hinzufugen von cacheWidth zu Bildern und Verschieben des BlocBuilder, sodass er nur das Warenkorb-Icon-Badge umschliesst) fielen die Frame-Zeiten von 45ms auf 8ms, und der Speicherverbrauch sank um 120MB. Der gesamte Prozess dauerte zwei Tage fokussiertes Profiling und schrittweise Anderungen.

Fortgeschrittene Techniken

Slivers fur komplexes Scrolling

Wenn Sie heterogene scrollbare Inhalte haben (Header, Grids, Listen), verwenden Sie CustomScrollView mit Slivers anstatt scrollbare Widgets zu verschachteln.

dart
CustomScrollView(
  slivers: [
    SliverAppBar(floating: true, title: Text('Produkte')),
    SliverGrid(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
      ),
      delegate: SliverChildBuilderDelegate(
        (context, i) => ProductCard(products[i]),
        childCount: products.length,
      ),
    ),
  ],
)

ValueListenableBuilder statt setState

Fur feingranulare Updates baut ValueListenableBuilder nur den Unterbaum neu auf, der vom Wert abhangt.

dart
final _counter = ValueNotifier<int>(0);

// Nur der Unterbaum dieses Builders wird neu gebaut
ValueListenableBuilder<int>(
  valueListenable: _counter,
  builder: (context, value, child) {
    return Text('Zahler: $value');
  },
)

saveLayer-Ausloser vermeiden

Bestimmte Widgets rufen implizit saveLayer auf, was einen Offscreen-Buffer allokiert. Die grossten Ubeltater: Opacity, ShaderMask, ColorFiltered, ClipRRect (mit clipBehavior: Clip.antiAliasWithSaveLayer). Verwenden Sie den "highlight saveLayer"-Toggle in DevTools, um sie zu finden.

Performance-Checkliste

Gehen Sie vor jedem Release diese Checkliste durch:

  • [ ] Scroll-Performance im Profile-Modus (nicht Debug) profiliert
  • [ ] Keine Frames uberschreiten 16ms in der Timeline bei normaler Nutzung
  • [ ] Alle Listen verwenden .builder oder Sliver-Aquivalente
  • [ ] Bilder geben cacheWidth/cacheHeight an
  • [ ] const-Konstruktoren werden uberall verwendet, wo moglich
  • [ ] Keine schweren Berechnungen in build()-Methoden
  • [ ] Alle Animation-Controller in dispose() aufgeraumt
  • [ ] Alle Stream-Subscriptions in dispose() abgemeldet
  • [ ] RepaintBoundary um animierte/haufig wechselnde Widgets hinzugefugt
  • [ ] Keine unnotigen Opacity-Widgets, die grosse Unterbaume umschliessen
  • [ ] Speicher-Snapshots zeigen kein Wachstum nach wiederholter Navigation
  • [ ] Shader-Aufwarmen fur kritische Nutzerpfade konfiguriert
  • [ ] App-Grosse mit flutter build --analyze-size analysiert
  • [ ] Release-Build auf dem leistungsschwachsten Zielgerat getestet

Fazit

Performance-Optimierung ist keine einmalige Aufgabe. Es ist eine Disziplin. Erst messen, den grossten Engpass beheben, die Verbesserung verifizieren und wiederholen. Die Werkzeuge existieren. Die Muster sind gut dokumentiert. Was eine flussige App von einer ruckelnden unterscheidet, ist die Entscheidung, Performance als Feature zu priorisieren und nicht als nachtraglichen Gedanken.

Mochten Sie ein praxisnahes Performance-Audit fur Ihre Flutter-App? Lassen Sie uns die Engpasse gemeinsam identifizieren und beheben.

Verwandte Artikel

Haben Sie ein Flutter-Projekt?

Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

Kontakt aufnehmen