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

# Flutter Performance-Optimierung: Vollstandiger Leitfaden

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 = class="code-number">0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          class=class="code-string">"code-comment">// Dieses teure Widget wird bei jeder _counter-Anderung neu gebaut
          ExpensiveHeader(),
          ExpensiveProductList(),
          Text(class="code-string">'Zahler: $_counter'),
          ElevatedButton(
            onPressed: () => setState(() => _counter++),
            child: Text(class="code-string">'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(),
          class=class="code-string">"code-comment">// 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 = class="code-number">0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(class="code-string">'Zahler: $_counter'),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: Text(class="code-string">'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
class=class="code-string">"code-comment">// Schlecht - neue Instanz bei jedem Build
Container(
  padding: EdgeInsets.all(class="code-number">16),
  child: Text(class="code-string">'Hallo'),
)

class=class="code-string">"code-comment">// Gut - Compile-Time-Konstante, wird beim Rebuild uebersprungen
const Padding(
  padding: EdgeInsets.all(class="code-number">16),
  child: Text(class="code-string">'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
class=class="code-string">"code-comment">// Schlecht - erstellt alle class="code-number">10.000 Elemente sofort
ListView(
  children: List.generate(class="code-number">10000, (i) => ProductCard(product: products[i])),
)

class=class="code-string">"code-comment">// 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
class=class="code-string">"code-comment">// Schlecht - volle Auflosung wird in den Speicher geladen
Image.network(class="code-string">'https:class=class="code-string">"code-comment">//example.com/grosses-foto.jpg')

class=class="code-string">"code-comment">// Gut - nur das dekodieren, was benotigt wird
Image.network(
  class="code-string">'https:class=class="code-string">"code-comment">//example.com/grosses-foto.jpg',
  cacheWidth: class="code-number">400,  class=class="code-string">"code-comment">// 2x Anzeigebreite fuer Retina
  cacheHeight: class="code-number">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
class=class="code-string">"code-comment">// Schlecht - blockiert die UI
final data = jsonDecode(riesiqerJsonString);

class=class="code-string">"code-comment">// 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
class=class="code-string">"code-comment">// Schlecht - erzwingt Offscreen-Buffer fur gesamten Unterbaum
Opacity(
  opacity: class="code-number">0.5,
  child: GrossesKomplexesWidget(),
)

class=class="code-string">"code-comment">// Besser - wenn nur Text-/Icon-Transparenz benotigt wird
Text(class="code-string">'Hallo', style: TextStyle(color: Colors.black.withOpacity(class="code-number">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
class=class="code-string">"code-comment">// 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
class=class="code-string">"code-comment">// 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]),
  );
}

class=class="code-string">"code-comment">// 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
class=class="code-string">"code-comment">// Schlecht innerhalb einer ListView
IntrinsicHeight(
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [LeftPanel(), RightPanel()],
  ),
)

class=class="code-string">"code-comment">// Besser - feste Hohe oder LayoutBuilder verwenden
SizedBox(
  height: class="code-number">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(() { class=class="code-string">"code-comment">/* aktualisieren */ });
        });
      }
    
      class=class="code-string">"code-comment">// Wenn Sie das vergessen, halt die Subscription eine Referenz
      class=class="code-string">"code-comment">// 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: class="code-number">300),
        );
      }
    
      @override
      void dispose() {
        _controller.dispose(); class=class="code-string">"code-comment">// Kritisch!
        super.dispose();
      }
    }

    Closures, die BuildContext einfangen:

    dart
    class=class="code-string">"code-comment">// Gefahrlich - die Closure fangt den Context ein, der den Element-Baum halt
    void _onTap(BuildContext context) {
      Future.delayed(Duration(seconds: class="code-number">5), () {
        class=class="code-string">"code-comment">// Wenn das Widget wahrend der 5s disposed wird, ist dieser Context ungueltig
        Navigator.of(context).push(...);
      });
    }
    
    class=class="code-string">"code-comment">// Sicherer - mounted pruefen
    void _onTap(BuildContext context) {
      Future.delayed(Duration(seconds: class="code-number">5), () {
        if (!mounted) return;
        Navigator.of(context).push(...);
      });
    }

    Den Memory-Profiler verwenden

  • Offnen Sie DevTools und gehen Sie zum **Memory**-Tab.
  • Machen Sie einen **Snapshot** vor der Interaktion.
  • Fuhren Sie die Aktion aus (z.B. zu einem Bildschirm navigieren und zuruck, 5-mal).
  • Machen Sie einen weiteren Snapshot.
  • 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(class="code-string">'Produkte')),
        SliverGrid(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: class="code-number">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>(class="code-number">0);
    
    class=class="code-string">"code-comment">// Nur der Unterbaum dieses Builders wird neu gebaut
    ValueListenableBuilder<int>(
      valueListenable: _counter,
      builder: (context, value, child) {
        return Text(class="code-string">'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