Perché le performance contano
Flutter è noto per la sua capacità di raggiungere i 60 (o 120) fps, ma scrivere codice non ottimizzato può facilmente introdurre jank, ovvero scatti visibili durante scroll e animazioni. In questo articolo vediamo le tecniche più efficaci per mantenere le tue app fluide e gli strumenti per misurare i miglioramenti.
Comprendere il rendering: build, layout e paint
Ogni frame in Flutter attraversa tre fasi principali:
- Build: ricostruzione dei widget
- Layout: calcolo di dimensioni e posizioni
- Paint: disegno effettivo sullo schermo
L'obiettivo è ridurre il lavoro inutile in ciascuna di queste fasi. Un budget di 16 ms per frame (a 60 fps) è il limite da rispettare.
1. Usa const ovunque possibile
I widget const vengono creati una sola volta e riutilizzati, evitando ricostruzioni superflue.
// Male: nuovo widget a ogni build
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16),
child: Text('Ciao'),
);
}
// Bene: widget costante riutilizzato
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(16),
child: Text('Ciao'),
);
}
Attiva la lint prefer_const_constructors nel tuo analysis_options.yaml per essere avvisato automaticamente.
2. Limita l'ambito delle ricostruzioni
Un errore comune è chiamare setState su un widget troppo grande. Isola la parte che cambia in un widget separato.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
const ExpensiveHeader(), // non si ricostruisce
Text('$_count'),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('Incrementa'),
),
],
);
}
}
Grazie al const su ExpensiveHeader, Flutter non lo ricostruisce nemmeno quando il contatore cambia.
3. Usa ListView.builder per liste lunghe
Non costruire mai una lista lunga con Column o ListView con figli espliciti: caricheresti tutti gli elementi in memoria. ListView.builder crea solo gli elementi visibili.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index].name));
},
)
4. Isola le ridipinture con RepaintBoundary
Quando hai un'animazione o un widget che cambia frequentemente accanto a contenuto statico complesso, RepaintBoundary impedisce che la ridipintura si propaghi.
RepaintBoundary(
child: SpinningLogo(), // ridipinge solo se stesso
)
Usalo con criterio: un eccesso di boundary aumenta l'uso di memoria.
5. Attenzione a opacità e clipping
Usare Opacity per animazioni è costoso perché richiede un layer separato. Preferisci AnimatedOpacity solo quando necessario o, per le immagini, usa direttamente colori con canale alfa. Allo stesso modo evita Clip.antiAliasWithSaveLayer se non indispensabile.
Misurare con Flutter DevTools
Le tecniche servono a poco senza misurazioni. Flutter DevTools offre strumenti dedicati:
- Performance view: mostra il timeline dei frame e ti segnala quelli che superano i 16 ms.
- CPU profiler: identifica i metodi più costosi.
- Widget rebuild stats: conta quante volte ogni widget si ricostruisce.
Avvia l'app in modalità profile (non debug, che ha overhead) per dati realistici:
flutter run --profile
Attiva inoltre l'overlay delle performance per vedere i grafici di build e raster direttamente sul dispositivo:
MaterialApp(
showPerformanceOverlay: true,
home: const HomePage(),
)
Verificare le ricostruzioni con il debug flag
Puoi loggare ogni ricostruzione dei widget per scovare quelli che si ridisegnano troppo spesso:
import 'package:flutter/rendering.dart';
void main() {
debugPrintRebuildDirtyWidgets = true;
runApp(const MyApp());
}
Conclusioni
L'ottimizzazione delle performance in Flutter non è una magia, ma una disciplina: usa const, isola le ricostruzioni, sfrutta i builder per le liste e misura sempre con DevTools in modalità profile. Piccoli accorgimenti applicati con costanza fanno la differenza tra un'app che scatta e una fluida come la seta.
