Praktyczne techniki optymalizacji wydajności aplikacji Flutter
Aplikacje Flutter często cierpią na zacinanie się z powodu zbędnych przebudowań, operacji w wątku UI i nieoptymalnej pracy z listami. W artykule omówiono kluczowe błędy i sposoby ich naprawy: od lokalizacji aktualizacji po poprawne użycie builderów i animacji. Te techniki pozwalają utrzymać 60 FPS nawet na słabszych urządzeniach.
Minimalizacja zbędnych przebudowań UI
Zbędne przebudowy to główna przyczyna spadku wydajności. setState() przebudowuje całe poddrzewo StatefulWidget, w tym ciężkie listy i obrazy. Prowadzi to do obliczeń układu i spadku FPS.
Problemowy kod:
class MyScreen extends StatefulWidget {
@override
State<MyScreen> createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $counter'),
ElevatedButton(
onPressed: () {
setState(() {
counter++; // przebudowuje CAŁE drzewo
});
},
child: Text('Increment'),
),
],
);
}
}
Rozwiązanie: ValueListenableBuilder aktualizuje tylko zależną część UI.
class MyScreen extends StatelessWidget {
final ValueNotifier<int> counter = ValueNotifier(0);
@override
Widget build(BuildContext context) {
return Column(
children: [
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (_, value, __) {
return Text('Counter: $value');
},
),
ElevatedButton(
onPressed: () => counter.value++,
child: const Text('Increment'),
),
],
);
}
}
Podobnie działają StreamBuilder, Selector z provider lub BlocBuilder. Obszar przerysowania jest minimalizowany, obciążenie CPU spada.
Użycie konstruktorów const
Bez const każda przebudowa tworzy nowe obiekty widżetów, obciążając GC i CPU. W listach z setkami ikon, tekstów i paddingów jest to krytyczne.
Optymalny wariant:
Widget build(BuildContext context) {
return const Column(
children: [
Text('Hello'),
Icon(Icons.home),
Padding(
padding: EdgeInsets.all(8),
child: Text('World'),
),
],
);
}
Flutter ponownie używa obiektów const, alokacje są redukowane. Wzrost wydajności jest zauważalny w złożonych UI z tysiącami małych widżetów.
Przenoszenie logiki poza build()
Sortowanie, filtrowanie lub parsowanie w build() wykonywane jest przy każdym wywołaniu — do 60 razy/sek. Dla 60 FPS na klatkę pozostaje 16 ms, ciężkie operacje powodują zacinanie.
Prawidłowe podejście:
late List<int> filtered;
@override
void initState() {
super.initState();
final sorted = [...items]..sort((a, b) => a.compareTo(b));
filtered = sorted.where((e) => e > 10).toList();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: filtered.length,
itemBuilder: (_, i) => Text('${filtered[i]}'),
);
}
Logika wykonywana jest jednorazowo w initState lub warstwie biznesowej. build() pozostaje deklaratywny.
Leniwie listy z ListView.builder
ListView z children: [...] tworzy wszystkie elementy od razu, nawet niewidoczne. Przy 100+ elementach pamięć i pierwszy render cierpią.
Rekomendacja:
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
subtitle: Text(item.subtitle),
);
},
);
}
Elementy generowane są w miarę przewijania, pamięć jest zoptymalizowana, recykling działa efektywnie.
Klucze dla stabilności list
Bez key Flutter nie dopasowuje elementów przy aktualizacjach listy, powodując zbędne przebudowy i wizualne artefakty.
Krytyczne dla:
- Zmiany kolejności elementów
- Usuwania/wstawiania
- Animacji przejść
ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) {
final item = items[index];
return ListTile(
key: ValueKey(item.id),
title: Text(item.title),
);
},
);
ValueKey(item.id) zapewnia poprawne aktualizowanie.
Animacje bez setState
Timer.periodic z setState co 16–100 ms przeciąża wątek UI.
Standardowe narzędzie:
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (_, __) {
return LinearProgressIndicator(value: controller.value);
},
);
}
AnimationController synchronizuje się z FPS urządzenia, aktualizując minimum UI.
Podział na małe widżety
Jeden ogromny StatefulWidget przebudowuje się całkowicie przy każdej zmianie.
Zalety kompozycji:
- Lokalizacja przebudów
- Ponowne użycie kodu
- Lepsza czytelność i testy
Widget build(BuildContext context) {
return Column(
children: const [
Header(),
Expanded(child: ItemsList()),
Footer(),
],
);
}
class Header extends StatelessWidget {
const Header();
@override
Widget build(BuildContext context) {
return Text('Header');
}
}
Future poza build()
FutureBuilder z future: fetchData() w build() uruchamia zapytania przy każdej przebudowie.
late Future dataFuture;
@override
void initState() {
super.initState();
dataFuture = fetchData();
}
Widget build(BuildContext context) {
return FutureBuilder(
future: dataFuture,
builder: (_, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return Text(snapshot.data.toString());
},
);
}
Future tworzone jest raz, UI reaguje na zmiany.
Co jest ważne
- Lokalizuj aktualizacje: ValueListenableBuilder, AnimatedBuilder zamiast globalnego setState.
- const wszędzie: Redukuje alokacje o 50–70% w złożonych UI.
- Logika w initState: build() — tylko deklaratywny opis.
- ListView.builder + klucze: Dla stabilnych dynamicznych list.
- Kompozycja widżetów: Małe StatelessWidget zamiast monolitów.
— Editorial Team
Brak komentarzy.