Flutter-Performance-Optimierung 2026: Impeller, Rebuilds und Best Practices

Wie Flutter-Apps 2026 konstante 60 oder 120 fps halten: mit Impeller, disziplinierten Widget-Rebuilds, RepaintBoundary und DevTools-Profiling.

Flutter-Performance-Optimierung mit Impeller und reduzierten Widget-Rebuilds

Die Optimierung der Flutter-Performance im Jahr 2026 beruht auf zwei Veränderungen, die den Bau flüssiger Apps neu geprägt haben: Die Rendering-Engine Impeller ist auf iOS und Android inzwischen standardmäßig aktiv, und das Rebuild-Modell des Frameworks belohnt Entwickler, die Widget-Bäume klein und unveränderlich halten. Teams, die beide Hebel nutzen, halten konstante 60 oder 120 fps, ohne jeden Frame von Hand zu justieren.

Die wirkungsvollste Änderung

Der größte Gewinn in den meisten Flutter-Apps besteht darin, const-Konstruktoren hinzuzufügen und große build-Methoden aufzuteilen, damit ein setState-Aufruf nur einen einzigen Button neu zeichnet statt eines ganzen Bildschirms.

Warum Impeller die Performance-Basislinie von Flutter neu setzt

Impeller ist die Rendering-Engine von Flutter und hat Skia als Standard auf den mobilen Plattformen abgelöst. Der Unterschied zählt vor allem in den ersten Sekunden einer Animation. Skia kompilierte seine Shader zur Laufzeit, beim ersten Auftreten eines Effekts auf dem Bildschirm, was das berüchtigte Ruckeln beim ersten Durchlauf erzeugte, für das Flutter-Apps kritisiert wurden. Impeller kompiliert diese Shader vorab, während des Builds, sodass der allererste Frame einer Animation genauso flüssig ist wie der hundertste.

Impeller zielt außerdem direkt auf moderne Grafik-APIs: Metal auf iOS und Vulkan auf Android, mit einem OpenGL-Rückfall für ältere Android-Hardware. Die Dokumentation der Impeller-Engine auf GitHub beschreibt die Rendering-Architektur und die Matrix der Plattformunterstützung.

| Aspekt | Skia (veraltet) | Impeller (Standard 2026) | |--------|---------------|-------------------------| | Shader-Kompilierung | Zur Laufzeit, verursacht Ruckeln beim ersten Durchlauf | Vorab, beim Build | | iOS-Backend | OpenGL / Metal | Metal | | Android-Backend | OpenGL | Vulkan, OpenGL-Rückfall | | Flüssigkeit der ersten Animation | Gelegentliches Ruckeln | Konstant |

Für die meisten Projekte ist die Migration kostenlos: Aktuelle Flutter-Versionen aktivieren Impeller automatisch, und die alten Workarounds zum Shader-Warmup (--purge-persistent-cache, gebündelte .sksl-Dateien) werden nicht mehr benötigt und können gelöscht werden. Auch benutzerdefinierte FragmentProgram-Shader, die gegen die von Flutter unterstützte GLSL-Teilmenge geschrieben sind, funktionieren weiter, da Impeller sie über dieselbe Offline-Pipeline kompiliert wie die eigenen Effekte des Frameworks.

Zu prüfen, welche Engine eine App verwendet, kostet eine Zeile: Ein Debug-Build gibt beim Start das aktive Backend aus, und der DevTools-Inspector meldet es in den Rendering-Details der App. Sobald Impeller bestätigt ist, wandert die restliche Performance-Arbeit hinauf in die Widget-Schicht, wo das Framework weit häufiger neu baut, als sich der Bildschirm tatsächlich ändert. Genau darauf konzentriert sich der Rest dieses Leitfadens, denn eine schnelle Engine kann einen Widget-Baum nicht retten, der sich hunderte Male pro Sekunde neu zeichnet.

Flutter-Rebuilds mit const und Widget-Aufteilung reduzieren

Jedes setState markiert sein Widget als veraltet und führt build für dieses Element und seinen Teilbaum erneut aus. Der Trick besteht darin, diesen Teilbaum so schmal wie möglich zu halten. Zwei Gewohnheiten leisten den Großteil der Arbeit: statische Widgets als const zu markieren und den Zustand nach unten zu schieben, sodass der zustandsbehaftete Teil klein bleibt.

Ein const-Widget wird zu einer einzigen Instanz kanonisiert, sodass Flutter beim Rebuild eines Elternteils die identische Referenz vergleicht, keine Änderung erkennt und das Neuzeichnen dieses Zweigs komplett überspringt.

product_screen.dartdart
// A cart update repaints the badge, not the header.

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

  
  State<ProductScreen> createState() => _ProductScreenState();
}

class _ProductScreenState extends State<ProductScreen> {
  int _cartCount = 0;

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ProductHeader(),         // const: never rebuilds on setState
        CartBadge(count: _cartCount),   // only this subtree rebuilds
        FilledButton(
          onPressed: () => setState(() => _cartCount++),
          child: const Text('Add to cart'),
        ),
      ],
    );
  }
}

Den Lint prefer_const_constructors in der analysis_options.yaml zu aktivieren macht aus dieser Disziplin eine Compiler-Prüfung, sodass const-Gelegenheiten nie stillschweigend verloren gehen. Die verwandte Regel prefer_const_literals_to_create_immutables erweitert dieselbe Garantie auf Listen und Maps, die an Widgets übergeben werden.

Widget-Keys verdienen hier ebenfalls Erwähnung. Wenn eine Liste umsortiert wird oder ein Widget denselben Typ behält, aber seine Identität ändert, lässt ein ValueKey oder ObjectKey Flutter das alte Element der richtigen neuen Widget-Instanz zuordnen, statt den Zustand abzureißen und neu aufzubauen. Fehlende Keys sind eine häufige Ursache für verlorene Scroll-Positionen und zurückgesetzte Animationen in dynamischen Listen, und die dadurch verursachte zusätzliche Rebuild-Arbeit wird leicht übersehen.

Rebuilds auf die geänderten Daten begrenzen

Wenn der Zustand weit oben im Baum liegt, baut sich selbst ein gut aufgeteiltes Layout viel neu auf. Die Lösung besteht darin, einen Wert zu senden und nur ein einziges Widget darauf hören zu lassen. Flutter liefert ValueNotifier und ValueListenableBuilder genau dafür, ohne dass ein externes Paket nötig ist.

article_list.dartdart
// A scroll-to-top button that rebuilds alone, not the whole page.

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

  
  State<ArticleList> createState() => _ArticleListState();
}

class _ArticleListState extends State<ArticleList> {
  final _showFab = ValueNotifier<bool>(false);
  final _controller = ScrollController();

  
  void initState() {
    super.initState();
    // Update the notifier, not setState: the list never rebuilds
    _controller.addListener(() => _showFab.value = _controller.offset > 400);
  }

  
  void dispose() {
    _showFab.dispose();
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: _controller,
        itemCount: 200,
        itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
      ),
      floatingActionButton: ValueListenableBuilder<bool>(
        valueListenable: _showFab,
        builder: (_, visible, __) => visible
            ? FloatingActionButton(
                onPressed: () => _controller.jumpTo(0),
                child: const Icon(Icons.arrow_upward),
              )
            : const SizedBox.shrink(),
      ),
    );
  }
}

Bibliotheken zur Zustandsverwaltung verallgemeinern dieselbe Idee. Riverpods ref.watch(provider.select(...)) und das Selector-Widget von Provider bauen beide nur dann neu auf, wenn sich ein ausgewählter Teil des Zustands ändert, was auf datenlastigen Bildschirmen den Ausschlag zwischen flüssig und ruckelig gibt. Die Abwägungen zwischen diesen Bibliotheken behandelt der Leitfaden zur Flutter-Zustandsverwaltung 2026.

Auf den Rebuild-Umfang achten

Ein setState-Aufruf nahe der Wurzel eines Bildschirms oder das Einhüllen einer ganzen Seite in einen Consumer auf oberster Ebene zeichnet bei jeder Änderung still hunderte Widgets neu. Profilieren Sie mit dem Rebuild-Zähler in den DevTools, bevor Sie annehmen, ein Bildschirm sei günstig.

Bereit für deine Flutter-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Teure Repaints mit RepaintBoundary isolieren

Rebuild und Repaint sind getrennte Phasen. Ein Widget kann den Rebuild überspringen und sich dennoch neu zeichnen, weil ein unruhiger Nachbar seine Ebene teilt. RepaintBoundary gibt einem Teilbaum eine eigene Ebene, sodass ein animiertes Diagramm statische Karten nicht zwingen kann, jeden Frame neu zu zeichnen.

dashboard.dartdart
// The live chart repaints on its own layer, cards stay untouched.

class Dashboard extends StatelessWidget {
  const Dashboard({required this.ticker, super.key});
  final Stream<double> ticker;

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // SummaryCards is static, so isolate it from the animation below
        const RepaintBoundary(child: SummaryCards()),
        RepaintBoundary(
          child: StreamBuilder<double>(
            stream: ticker,
            builder: (_, snap) => LivePriceChart(value: snap.data ?? 0),
          ),
        ),
      ],
    );
  }
}

RepaintBoundary ist nicht kostenlos: Jede Instanz belegt eine Ebene, sodass es mehr schadet als nützt, es überall zu verstreuen. Man greift dazu bei wirklich animierten Bereichen und langen Scroll-Listen und bestätigt den Gewinn in der Timeline. Die RepaintBoundary-API-Referenz dokumentiert, wie das Debug-Repaint-Overlay hervorhebt, welche Ebenen tatsächlich neu gezeichnet werden.

Die build-Methode günstig und ohne Seiteneffekte halten

Eine build-Methode läuft bei jedem Rebuild, also vervielfacht sich alles Teure darin. Die Regel lautet: build soll Zustand lesen und Widgets zurückgeben, mehr nicht. Einen ScrollController, AnimationController oder ein Future innerhalb von build zu erzeugen, legt es bei jedem Frame neu an und verliert die vorherige Instanz. Diese Objekte gehören in initState oder in ein late final-Feld, das in dispose freigegeben wird.

Schwere synchrone Arbeit ist die zweite Falle. JSON zu parsen, eine große Liste zu filtern oder Daten innerhalb von build zu formatieren, führt diese Arbeit bei jedem Repaint erneut aus, selbst wenn sich die zugrunde liegenden Daten nicht geändert haben. Berechnen Sie das Ergebnis einmal, wenn sich die Eingabe ändert, und speichern Sie es zwischen, oder verschieben Sie es in einen FutureBuilder, damit das Framework nicht einen Frame lang darauf wartet. Für aus anderen Werten abgeleitete Werte memoisieren Sie sie hinter einem Getter, der nur bei geänderten Abhängigkeiten neu berechnet.

Das Opacity-Widget ist ein konkreter Übeltäter, den man benennen sollte. Einen Teilbaum in Opacity zu hüllen, erzwingt ein saveLayer außerhalb des Bildschirms, eine der teuersten Operationen der Pipeline. Für eine Überblendung sind AnimatedOpacity oder eine FadeTransition günstiger, und für eine feste Tönung vermeidet das Anwenden der Farbe auf Paint-Ebene über eine foregroundDecoration oder einen Shader die Ebene ganz. Dieselbe Vorsicht gilt für ClipPath und ClipRRect mit Antialiasing auf großen Flächen, die im Hintergrund ebenfalls ein saveLayer auslösen.

Bevorzugen Sie schließlich const-Sammlungen und Callbacks, die nicht bei jedem Build neue Closures einfangen. Ein als Standard übergebenes const [] und eine Methodenreferenz statt einer Inline-Lambda halten die Gleichheitsprüfungen von Widgets günstig, was den zuvor beschriebenen const-Kurzschluss überhaupt erst auslösen lässt.

Lange Listen und Bilder effizient aufbauen

Lange Feeds und Bildraster sind die Stelle, an der naiver Flutter-Code Frames verliert. Zwei Regeln decken die meisten Fälle ab: Zeilen faul aufbauen und Bilder in der Größe dekodieren, in der sie angezeigt werden, statt in ihrer Quellauflösung.

feed.dartdart
// Lazy rows plus right-sized image decoding keep scrolling at 60fps.

class Feed extends StatelessWidget {
  const Feed({required this.posts, super.key});
  final List<Post> posts;

  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: posts.length,     // build on demand, not all at once
      cacheExtent: 600,            // pre-build past the viewport edge
      itemBuilder: (context, index) {
        final post = posts[index];
        return ListTile(
          leading: Image.network(
            post.thumbnailUrl,
            width: 48,
            height: 48,
            cacheWidth: 96,        // decode at display size, save memory
          ),
          title: Text(post.title),
        );
      },
    );
  }
}

Ein Bild in voller Auflösung mit 3000 Pixeln, das in einen 48-Pixel-Avatar gequetscht wird, verschwendet bei jedem einzelnen Speicher und Dekodierzeit. cacheWidth zu setzen, weist die Engine an, in der Zielgröße zu dekodieren, was allein den Speicherbedarf einer Liste um eine Größenordnung senken kann. Die Best Practices zur Performance von Flutter bündeln weitere Hinweise zu Opacity, saveLayer und Clip-Nutzung, die demselben Prinzip folgen.

Flutter-Optimierung 2026 mit DevTools messen

Jede obige Regel sollte verifiziert und nicht angenommen werden. Flutter DevTools ist die verlässliche Quelle. Das Performance-Overlay (P in der Debug-Konsole oder flutter run --profile) zeichnet zwei Graphen, UI und Raster, und jeder Balken, der die grüne Linie überschreitet, ist ein verlorener Frame. Die Timeline-Ansicht ordnet dann jeden langsamen Frame einem bestimmten build-, Layout- oder Paint-Aufruf zu.

Profilieren Sie immer im --profile-Modus auf einem physischen Gerät. Debug-Builds führen unoptimiertes Dart aus und blähen jede Messung auf, sodass ein Bildschirm, der im Debug ruckelt, kompiliert oft einwandfrei läuft. Der DevTools-Performance-Leitfaden führt durch das Lesen des Frame-Diagramms und das Aufspüren von Ruckelquellen, und dieselbe Profiling-Routine taucht in Vorstellungsgesprächen über Flutter-Animationen und Rendering auf.

Eine schnelle Profiling-Schleife

Im Profile-Modus starten, die ruckelige Interaktion reproduzieren, die Timeline öffnen und die Frames nach Dauer sortieren. Der schlechteste Frame verweist fast immer auf einen einzigen überdimensionierten Rebuild oder ein nicht zwischengespeichertes Bild, was weit schneller ist als Raten.

Der Flutter-Technologie-Hub verknüpft die Themen Zustandsverwaltung, Testing und Rendering, die eine produktionsreife Performance-Strategie abrunden.

Fazit

Flutter-Performance im Jahr 2026 geht weniger um Mikro-Tricks als um eine kurze Liste konsequent angewandter Gewohnheiten:

  • Mit Impeller ausliefern und alten Shader-Warmup-Code löschen; das Problem des Ruckelns im ersten Frame ist auf Engine-Ebene gelöst.
  • Statische Widgets als const markieren und dies mit dem Lint prefer_const_constructors erzwingen.
  • Zustandsbehaftete Widgets klein aufteilen, sodass jedes setState den schmalstmöglichen Teilbaum neu zeichnet.
  • Sich ändernde Werte über ValueListenableBuilder oder eine select-artige API senden, statt ganze Bildschirme neu aufzubauen.
  • Wirklich animierte Bereiche in ein RepaintBoundary hüllen, aber messen, bevor man mehr hinzufügt.
  • Listen mit ListView.builder aufbauen und Bilder mit cacheWidth in Anzeigegröße dekodieren.
  • Im --profile-Modus auf echter Hardware profilieren und DevTools, nicht die Intuition, entscheiden lassen, was als Nächstes zu optimieren ist.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#flutter
#performance
#impeller
#best-practices
#dart
#mobile

Teilen

Verwandte Artikel