Flutter Performance-Optimierung: Vollständiger Leitfaden
# 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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
flutter run --profileProfilieren 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:
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:
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:
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:
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:
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
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:
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.
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.
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:
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
Was ist Flutter? Ein vollständiger Leitfaden für Einsteiger
Erfahren Sie, was Flutter ist, wie es funktioniert und warum moderne Produktteams darauf setzen. Entdecken Sie Dart, Widget-Architektur und plattformübergreifende Entwicklung.
Clean Architecture in Flutter: Skalierbare Anwendungen entwickeln
Lernen Sie, Clean Architecture in Flutter praxisnah umzusetzen. Ein Leitfaden für Schichten, Dependency Management und testbaren Code.
Dart Best Practices: Sauberer und wartbarer Code
Lernen Sie praxiserprobte Dart-Methoden für lesbaren, testbaren und wartbaren Code.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen