Flutter-prestatieoptimalisatie in 2026: Impeller, rebuilds en best practices

Hoe Flutter-apps in 2026 constant 60 of 120 fps halen met Impeller, gedisciplineerde widget-rebuilds, RepaintBoundary en profilering met DevTools.

Flutter-prestatieoptimalisatie met Impeller en minder widget-rebuilds

De prestatieoptimalisatie van Flutter in 2026 rust op twee verschuivingen die veranderden hoe soepele apps worden gebouwd: de rendering-engine Impeller is nu standaard actief op iOS en Android, en het rebuild-model van het framework beloont ontwikkelaars die widgetbomen klein en onveranderlijk houden. Teams die beide hefbomen benutten, halen constant 60 of 120 fps zonder elke frame met de hand bij te stellen.

De meest impactvolle verandering

De grootste winst in de meeste Flutter-apps zit in het toevoegen van const-constructors en het opsplitsen van grote build-methoden, zodat een setState-aanroep één knop opnieuw tekent in plaats van een heel scherm.

Waarom Impeller de prestatiebasislijn van Flutter opnieuw zet

Impeller is de rendering-engine van Flutter en verving Skia als standaard op de mobiele platforms. Het verschil telt vooral in de eerste seconden van een animatie. Skia compileerde zijn shaders just-in-time, de eerste keer dat een effect op het scherm verscheen, wat het beruchte haperen bij de eerste keer opleverde waarom Flutter-apps werden bekritiseerd. Impeller compileert die shaders vooraf, tijdens de build, zodat de allereerste frame van een animatie net zo soepel is als de honderdste.

Impeller richt zich ook rechtstreeks op moderne grafische API's: Metal op iOS en Vulkan op Android, met een OpenGL-terugval voor oudere Android-hardware. De documentatie van de Impeller-engine op GitHub beschrijft de rendering-architectuur en de matrix van platformondersteuning.

| Aspect | Skia (verouderd) | Impeller (standaard 2026) | |--------|---------------|-------------------------| | Shader-compilatie | Tijdens runtime, veroorzaakt haperen bij eerste keer | Vooraf, tijdens de build | | iOS-backend | OpenGL / Metal | Metal | | Android-backend | OpenGL | Vulkan, OpenGL-terugval | | Soepelheid van de eerste animatie | Af en toe haperen | Constant |

Voor de meeste projecten is de migratie gratis: recente Flutter-versies schakelen Impeller automatisch in, en de oude workarounds voor shader-warmup (--purge-persistent-cache, gebundelde .sksl-bestanden) zijn niet meer nodig en kunnen worden verwijderd. Ook aangepaste FragmentProgram-shaders die zijn geschreven tegen de door Flutter ondersteunde GLSL-subset blijven werken, omdat Impeller ze compileert via dezelfde offline pipeline als de eigen effecten van het framework.

Nagaan welke engine een app draait, kost één regel: een debug-build print de actieve backend bij het opstarten, en de DevTools-inspector meldt het bij de rendering-details van de app. Zodra Impeller is bevestigd, verschuift het resterende prestatiewerk omhoog naar de widgetlaag, waar het framework veel vaker herbouwt dan het scherm daadwerkelijk verandert. Daar richt de rest van deze gids zich op, want een snelle engine kan een widgetboom die zichzelf honderden keren per seconde opnieuw tekent niet redden.

Flutter-rebuilds terugdringen met const en het opsplitsen van widgets

Elke setState markeert zijn widget als verouderd en voert build opnieuw uit voor dat element en zijn subboom. De truc is die subboom zo smal mogelijk te houden. Twee gewoonten doen het meeste werk: statische widgets als const markeren en de state naar beneden duwen zodat het deel met state klein blijft.

Een const-widget wordt gecanoniseerd tot één enkele instantie, zodat Flutter bij een rebuild van een ouder de identieke referentie vergelijkt, geen wijziging ziet en het opnieuw tekenen van die tak volledig overslaat.

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'),
        ),
      ],
    );
  }
}

De lint prefer_const_constructors inschakelen in analysis_options.yaml maakt van deze discipline een compilercontrole, zodat const-kansen nooit stilletjes terugvallen. De verwante regel prefer_const_literals_to_create_immutables breidt dezelfde garantie uit naar lijsten en maps die aan widgets worden doorgegeven.

Widget-keys verdienen hier ook een vermelding. Wanneer een lijst opnieuw wordt geordend of een widget hetzelfde type behoudt maar van identiteit verandert, laat een ValueKey of ObjectKey Flutter het oude element koppelen aan de juiste nieuwe widget-instantie in plaats van de state af te breken en opnieuw op te bouwen. Ontbrekende keys zijn een veelvoorkomende oorzaak van verloren scrollposities en gereset animaties in dynamische lijsten, en het extra rebuild-werk dat ze veroorzaken is makkelijk over het hoofd te zien.

Rebuilds beperken tot de gewijzigde data

Wanneer state hoog in de boom leeft, herbouwt zelfs een goed opgesplitste layout nog veel. De oplossing is een waarde uit te zenden en één widget ernaar te laten luisteren. Flutter levert ValueNotifier en ValueListenableBuilder precies hiervoor, zonder dat een extern package nodig is.

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 voor statebeheer veralgemenen hetzelfde idee. De ref.watch(provider.select(...)) van Riverpod en de Selector-widget van Provider herbouwen beide alleen wanneer een gekozen deel van de state verandert, wat op datadichte schermen de doorslag geeft tussen soepel en hakkelend. De afwegingen tussen die bibliotheken komen aan bod in de gids over statebeheer in Flutter in 2026.

Let op de reikwijdte van de rebuild

setState aanroepen dicht bij de wortel van een scherm, of een hele pagina in een Consumer op topniveau wikkelen, tekent stilletjes honderden widgets opnieuw bij elke wijziging. Profileer met de rebuild-teller in DevTools voordat je aanneemt dat een scherm goedkoop is.

Klaar om je Flutter gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Dure repaints isoleren met RepaintBoundary

Herbouwen en opnieuw tekenen zijn afzonderlijke fasen. Een widget kan de rebuild overslaan en toch opnieuw tekenen omdat een rumoerige buur zijn laag deelt. RepaintBoundary geeft een subboom een eigen laag, zodat een geanimeerde grafiek statische kaarten niet kan dwingen elke frame opnieuw te tekenen.

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 is niet gratis: elk exemplaar reserveert een laag, dus het overal rondstrooien schaadt meer dan het helpt. Reserveer het voor echt geanimeerde regio's en lange scrollende lijsten, en bevestig de winst in de timeline. De RepaintBoundary API-referentie documenteert hoe de debug-repaint-overlay markeert welke lagen daadwerkelijk opnieuw worden getekend.

De build-methode goedkoop en zonder neveneffecten houden

Een build-methode draait bij elke rebuild, dus alles wat duur is erin vermenigvuldigt zich. De regel is dat build de state moet lezen en widgets moet teruggeven, meer niet. Een ScrollController, AnimationController of Future binnen build aanmaken, herstelt die bij elke frame en lekt de vorige instantie. Die objecten horen thuis in initState of in een late final-veld, vrijgegeven in dispose.

Zwaar synchroon werk is de tweede valkuil. JSON parsen, een grote lijst filteren of datums formatteren binnen build voert dat werk bij elke repaint opnieuw uit, zelfs wanneer de onderliggende data niet is veranderd. Bereken het resultaat één keer wanneer de invoer verandert en cache het, of verplaats het naar een FutureBuilder zodat het framework geen frame lang wacht. Voor waarden die zijn afgeleid van andere waarden, memoïseer ze achter een getter die alleen herberekent wanneer zijn afhankelijkheden veranderen.

De Opacity-widget is een specifieke boosdoener die het benoemen waard is. Een subboom in Opacity wikkelen dwingt een saveLayer buiten het scherm af, een van de duurste bewerkingen in de pipeline. Voor een fade-animatie zijn AnimatedOpacity of een FadeTransition goedkoper, en voor een vaste tint vermijdt het toepassen van kleur op paint-niveau via een foregroundDecoration of een shader de laag helemaal. Dezelfde voorzichtigheid geldt voor ClipPath en ClipRRect met anti-aliasing op grote oppervlakken, die achter de schermen eveneens een saveLayer uitlokken.

Geef ten slotte de voorkeur aan const-collecties en callbacks die niet bij elke build verse closures vastleggen. Een als standaard doorgegeven const [] en een methodereferentie in plaats van een inline-lambda houden de gelijkheidscontroles van widgets goedkoop, waardoor de eerder beschreven const-kortsluiting daadwerkelijk in werking treedt.

Lange lijsten en afbeeldingen efficiënt opbouwen

Lange feeds en afbeeldingsrasters zijn waar naïeve Flutter-code frames verliest. Twee regels dekken de meeste gevallen: rijen lui opbouwen en afbeeldingen decoderen op de grootte waarop ze worden getoond in plaats van op hun bronresolutie.

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),
        );
      },
    );
  }
}

Een afbeelding op volledige resolutie van 3000 pixels die in een avatar van 48 pixels wordt geperst, verspilt geheugen en decodeertijd bij elk daarvan. cacheWidth instellen vertelt de engine om op de doelgrootte te decoderen, wat op zichzelf al de geheugenvoetafdruk van een lijst met een orde van grootte kan verkleinen. De best practices voor prestaties van Flutter bundelen verdere richtlijnen over Opacity, saveLayer en clip-gebruik die hetzelfde principe volgen.

Flutter-optimalisatie in 2026 meten met DevTools

Elke bovenstaande regel moet worden geverifieerd, niet aangenomen. Flutter DevTools is de bron van waarheid. De prestatie-overlay (P in de debug-console, of flutter run --profile) tekent twee grafieken, UI en raster, en elke balk die de groene lijn overschrijdt is een verloren frame. De timeline-weergave wijst vervolgens elke trage frame toe aan een specifieke build-, layout- of paint-aanroep.

Profileer altijd in --profile-modus op een fysiek apparaat. Debug-builds draaien ongeoptimaliseerd Dart en blazen elke meting op, zodat een scherm dat in debug hapert vaak perfect draait zodra het is gecompileerd. De DevTools-prestatiegids loopt door het lezen van de framegrafiek en het opsporen van haperbronnen, en dezelfde profileerreflex duikt op in sollicitatiegesprekken over Flutter-animaties en rendering.

Een snelle profileerlus

Draai in profile-modus, reproduceer de haperende interactie, open de timeline en sorteer de frames op duur. De slechtste frame wijst vrijwel altijd naar één te grote rebuild of een niet-gecachte afbeelding, wat veel sneller is dan gokken.

De Flutter-technologiehub verbindt de onderwerpen statebeheer, testen en rendering die een productieklare prestatiestrategie afronden.

Conclusie

Flutter-prestaties in 2026 draaien minder om microtrucs en meer om een korte lijst gewoonten die consistent worden toegepast:

  • Uitleveren op Impeller en verouderde shader-warmup-code verwijderen; het probleem van haperen bij de eerste frame is op engine-niveau opgelost.
  • Statische widgets als const markeren en dat afdwingen met de lint prefer_const_constructors.
  • Widgets met state klein opsplitsen, zodat elke setState de smalst mogelijke subboom opnieuw tekent.
  • Veranderende waarden uitzenden via ValueListenableBuilder of een select-achtige API in plaats van hele schermen te herbouwen.
  • Echt geanimeerde regio's in een RepaintBoundary wikkelen, maar meten voordat je er meer toevoegt.
  • Lijsten opbouwen met ListView.builder en afbeeldingen decoderen op weergavegrootte met cacheWidth.
  • Profileren in --profile-modus op echte hardware en DevTools, niet de intuïtie, laten beslissen wat als volgende geoptimaliseerd wordt.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

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

Delen

Gerelateerde artikelen