RepaintBoundary, const-widgets en rebuilds beperken
Beperk onnodige repaints en rebuilds met boundaries en const-constructors.
RepaintBoundary, const-widgets en rebuilds beperken is een gratis Mobiele ontwikkeling met Flutter-les op CoddyKit. Dit is les 3 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Mobiele ontwikkeling met Flutter. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Mobiele ontwikkeling met Flutter bevat in totaal 4 lessen.
Rebuilds tegenover Repaints
Werk aan Flutter-prestaties vindt plaats op drie lagen, en als je die door elkaar haalt, verspil je moeite:
- Rebuild —
build()wordt opnieuw uitgevoerd en levert een nieuwe widgetstructuur op. Goedkoop als widgets onveranderlijk en ondiep zijn, maar het kan zich door de structuur voortplanten. - Relayout — RenderObjects berekenen grootte en positie opnieuw. Dit wordt veroorzaakt door wijzigingen in constraints of children.
- Repaint — pixels worden opnieuw naar een layer gerasteriseerd. Dit is duur voor verlopen, schaduwen en complexe paden.
Deze les richt zich op twee verschillende verbeteringen: rebuilds beperken met const en een goede structuur, en repaints isoleren met RepaintBoundary. Ze lossen verschillende problemen op — gebruik de ene niet wanneer je de andere nodig hebt.
Waarom const-widgets rebuilds overslaan
Een const-widget wordt door de Dart-compiler gecanonicaliseerd: elke evaluatie van dezelfde const-expressie levert dezelfde instantie op. Wanneer een parent opnieuw wordt opgebouwd, vergelijkt Flutter de nieuwe child-widget met de oude. Als het dezelfde instantie is (identical(old, new) is waar), stopt Flutter meteen en bouwt het die subtree helemaal niet opnieuw op.
- Zonder
constmaakt elke parent-build een nieuwWidget-object, waardoor het element zijn child moet bijwerken. - Met
constwordt de canonieke instantie hergebruikt, waardoor de subtree volledig wordt overgeslagen.
Dit is de goedkoopste optimalisatie in Flutter — het kost je één trefwoord en voorkomt rebuilds van hele takken.
// Dart compile-time canonicalization: const instances are identical.
class Point {
final int x;
final int y;
const Point(this.x, this.y);
}
void main() {
const a = Point(1, 2);
const b = Point(1, 2);
// Both refer to the SAME canonical instance.
print(identical(a, b)); // true
final c = Point(1, 2); // runtime instance, not canonicalized
print(identical(a, c)); // false
}const toepassen in een widgetstructuur
Markeer elke widget die je kunt als const. De regel: een widget kan const zijn als al zijn constructorargumenten zelf compile-timeconstanten zijn. Statische labels, pictogrammen, padding en spacers zijn uitstekende kandidaten.
- Een
const-child in een parent die vaak opnieuw wordt opgebouwd, is de waardevolste plaats — de parent wordt opnieuw opgebouwd, maar de child wordt overgeslagen. - Schakel de lints
prefer_const_constructorsenprefer_const_literals_to_create_immutablesin, zodat de analyzer gemiste mogelijkheden markeert.
Hieronder verandert alleen de tekst van de live teller; de titel, scheidingslijn en het pictogram worden nooit opnieuw opgebouwd omdat ze const zijn.
class CounterPanel extends StatelessWidget {
final int count;
const CounterPanel({super.key, required this.count});
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Live Counter'), // const: skipped on rebuild
const Divider(), // const: skipped
Text('$count'), // dynamic: must rebuild
const Icon(Icons.timer), // const: skipped
],
);
}
}Wanneer const niet mogelijk is: de subtree omhoog verplaatsen
Je kunt een widget niet const maken als deze afhankelijk is van runtimewaarden (een themakleur, een opgehaalde tekenreeks of een callback-closure). Je kunt het opnieuw opbouwen ervan nog steeds voorkomen door het buiten de scope waarin rebuilds plaatsvinden omhoog te verplaatsen.
- Bouw de dure maar statische subtree één keer op in een parent, sla deze op in een veld of geef deze door als een
child-parameter, en laat de widget die opnieuw wordt opgebouwd dezelfde referentie hergebruiken. - Dit is precies het patroon dat
AnimatedBuilderenValueListenableBuildergebruiken met hun argumentchild: de child wordt één keer opgebouwd en ongewijzigd door elke animatieframe heen doorgegeven.
// The child is built ONCE and reused on every animation tick.
AnimatedBuilder(
animation: rotationController,
// 'child' is created a single time, not per-frame.
child: const ExpensiveLogo(),
builder: (context, child) {
return Transform.rotate(
angle: rotationController.value * 6.28,
child: child, // reused reference: no rebuild of ExpensiveLogo
);
},
)Hoe repaints zich voortplanten
Flutter combineert de UI in layers. Wanneer een RenderObject wordt gemarkeerd als gewijzigd voor het tekenen, tekent Flutter de hele layer waarin het zich bevindt opnieuw — niet alleen dat ene object. Standaard delen widgets naast elkaar vaak een layer.
- Een animatie die voortdurend opnieuw wordt getekend (een spinner, voortgangsbalk of knipperende cursor), markeert de layer in elk frame als gewijzigd.
- Als een zware statische widget (een grote afbeelding of complexe verloopachtergrond) die layer deelt, wordt deze ook in elk frame opnieuw gerasteriseerd — pure verspilling.
De oplossing is de veranderlijke widget een eigen layer te geven, zodat de repaints ervan beperkt blijven. Dat doet RepaintBoundary.
RepaintBoundary: een layer isoleren
RepaintBoundary omhult een subtree en dwingt deze in een afzonderlijke compositing-layer. Repaints binnen de boundary markeren de parent-layer niet langer als gewijzigd, en repaints erbuiten zorgen er niet langer voor dat de inhoud van de boundary opnieuw wordt gerasteriseerd.
- Omhul de widget die vaak opnieuw wordt getekend, zodat de activiteit ervan geïsoleerd blijft, OF
- Omhul de dure statische widget, zodat een drukke buurwidget deze niet meesleept in repaints van elk frame.
De boundary heeft echte kosten: een extra layer betekent extra geheugen en een compositingstap. Gebruik deze doelgericht waar de profiler verspilling door repaints laat zien — niet overal.
Stack(
children: [
// Heavy, static background — isolate it so the spinner can't
// force it to re-rasterize every frame.
const RepaintBoundary(
child: ComplexGradientBackground(),
),
// Constantly repainting overlay gets its own layer too.
const Center(
child: RepaintBoundary(
child: CircularProgressIndicator(),
),
),
],
)Lijsten voegen zelf RepaintBoundary toe
Een veelgestelde vraag: moet je elk item van een ListView omhullen met een RepaintBoundary? Meestal nee — ListView, GridView en andere op SliverList gebaseerde widgets omhullen elk item standaard al met een RepaintBoundary (aangestuurd door de vlag addRepaintBoundaries, die standaard true is).
- Dit betekent dat repaints tijdens het scrollen zich niet over alle zichtbare rijen verspreiden.
- Items nogmaals omhullen is overbodig en zorgt voor extra layer-overhead. Laat de standaardinstelling ingeschakeld.
- Je stelt
addRepaintBoundaries: falsealleen in voor uiterst goedkope items waarbij de extra layer meer kost dan deze bespaart.
ListView.builder(
itemCount: messages.length,
// addRepaintBoundaries defaults to true — each row is already isolated.
itemBuilder: (context, index) {
return MessageTile(message: messages[index]);
},
)setState bouwt de volledige build() opnieuw op
Als je setState aanroept, wordt de volledige build()-methode van de State gemarkeerd als gewijzigd. Alles wat die methode retourneert, wordt opnieuw opgebouwd — zelfs widgets die niets te maken hebben met de gewijzigde waarde. Op een groot scherm is dit de meest voorkomende oorzaak van haperingen.
- Hoe smaller je
build(), hoe goedkoper elkesetStateis. - Verplaats de veranderlijke state naar de kleinst mogelijke widget, zodat alleen die opnieuw wordt opgebouwd.
Hieronder bouwt één tik het hele scherm opnieuw op — inclusief de statische koptekst en voettekst — omdat ze in dezelfde build() als de teller staan.
class DashboardState extends State<Dashboard> {
int taps = 0;
@override
Widget build(BuildContext context) {
// setState rebuilds ALL of this, header and footer included.
return Column(
children: [
const ExpensiveHeader(),
Text('Taps: $taps'),
ElevatedButton(
onPressed: () => setState(() => taps++),
child: const Text('Tap'),
),
const ExpensiveFooter(),
],
);
}
}Beperken met ValueListenableBuilder
Als je alleen het deel wilt herbouwen dat afhankelijk is van een waarde, vervang je setState door een scoped listenable. ValueListenableBuilder bouwt alleen de builder-closure opnieuw op wanneer de waarde verandert; alles daarbuiten blijft staan.
- De koptekst, voettekst en omliggende layout worden één keer opgebouwd en nooit opnieuw opgebouwd wanneer de waarde verandert.
- Combineer dit met het argument
childom een statische subtree binnen de builder omhoog te verplaatsen.
Hiermee verander je een rebuild van het hele scherm in een gerichte rebuild van één widget.
final taps = ValueNotifier<int>(0);
@override
Widget build(BuildContext context) {
return Column(
children: [
const ExpensiveHeader(), // built once, never rebuilt
ValueListenableBuilder<int>(
valueListenable: taps,
builder: (context, value, child) => Text('Taps: $value'),
),
ElevatedButton(
onPressed: () => taps.value++, // no setState
child: const Text('Tap'),
),
const ExpensiveFooter(), // built once, never rebuilt
],
);
}Meten voordat je optimaliseert
Gok nooit — meet. Flutter levert tools die precies laten zien waar rebuilds en repaints plaatsvinden:
- Repaint Rainbow (DevTools /
debugRepaintRainbowEnabled): legt telkens wanneer een layer opnieuw wordt getekend een roterende randkleur over elke layer. Een widget waarvan de kleuren voortdurend flikkeren, wordt te vaak opnieuw getekend — een kandidaat voorRepaintBoundary. - Track Widget Builds / Rebuild Stats: telt hoe vaak elke widget opnieuw wordt opgebouwd en onthult widgets die veel vaker worden opgebouwd dan verwacht.
- Performance Overlay: toont de budgetten van de UI-thread (opbouw) en rasterthread (tekenen); pieken vertellen je welke thread de bottleneck is.
Optimaliseer alleen wat de profiler markeert. Een blind toegevoegde const of boundary kan meer kosten dan deze bespaart.
import 'package:flutter/rendering.dart';
void main() {
// Visualize layer repaints during development.
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}Een beslischecklist
Gebruik de drie tools in deze volgorde wanneer je haperingen tegenkomt:
- Te veel rebuilds? Voeg
consttoe waar argumenten constant zijn; verplaats statische subtrees omhoog via de parameterchild; beperk de scope van state metValueListenableBuilderin plaats van schermbredesetState. - Te veel repaints? Omhul de veranderlijke of dure widget met
RepaintBoundaryom de layer ervan te isoleren. - Weet je niet welke? Schakel eerst Rebuild Stats en Repaint Rainbow in.
Belangrijk onderscheid: const bestrijdt rebuilds; RepaintBoundary bestrijdt repaints. Als je de verkeerde gebruikt, blijft de bottleneck bestaan en voeg je mogelijk overhead toe.
Korte controle
Een scherm toont een statische, dure vervaagde achtergrondafbeelding. Daarboven staat een kleine CircularProgressIndicator die in elk frame animeert. De profiler laat zien dat de volledige achtergrond 60 keer per seconde opnieuw wordt gerasteriseerd. Wat is de juiste oplossing?
Samenvatting
Je hebt nu een nauwkeurig mentaal model om de renderkosten van Flutter te verlagen:
- Rebuilds worden beperkt door
const(canonieke instanties worden viaidenticalovergeslagen), door statische subtrees via de parameterchildomhoog te verplaatsen en door de scope van state te beperken metValueListenableBuilderin plaats van schermbredesetState. - Repaints worden geïsoleerd door
RepaintBoundary, die een subtree naar een eigen compositing-layer verplaatst, zodat veranderlijke en statische widgets elkaar niet langer beïnvloeden. - Lijsten voegen al per item repaint-boundaries toe — omhul items niet dubbel.
- Meet eerst met Rebuild Stats, Repaint Rainbow en de Performance Overlay; gebruik elke tool alleen waar de profiler aantoont dat deze resultaat oplevert.
De gouden regel: const bestrijdt rebuilds, RepaintBoundary bestrijdt repaints — stem de tool af op het symptoom.
Leer Dart met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 22
- Lessen
- 88
Veelgestelde vragen
Is de les “RepaintBoundary, const-widgets en rebuilds beperken” gratis?
Ja — je kunt hier op het web alle 3 lessen van het leerpad Mobiele ontwikkeling met Flutter, waaronder “RepaintBoundary, const-widgets en rebuilds beperken”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Mobiele ontwikkeling met Flutter bevat in totaal 4 lessen.
Wat leer ik in “RepaintBoundary, const-widgets en rebuilds beperken”?
Beperk onnodige repaints en rebuilds met boundaries en const-constructors. Je oefent met Mobiele ontwikkeling met Flutter door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met Mobiele ontwikkeling met Flutter te beginnen?
Ervaring vooraf is niet nodig. Mobiele ontwikkeling met Flutter op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.
Hoe lang duurt de les “RepaintBoundary, const-widgets en rebuilds beperken”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over Mobiele ontwikkeling met Flutter?
Ja. Elke les over Mobiele ontwikkeling met Flutter bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- De drie bomen: Widget, Element en RenderObject
- Jank profileren met de DevTools-tijdlijn
- RepaintBoundary, const-widgets en rebuilds beperken
- Shader-warm-up en migratie naar Impeller