Jank to momenty, w których animacje i scroll przestają być płynne. W Flutterze zwykle wygląda to jak „przycięcie” co kilka sekund albo spadki do 40–50 FPS na urządzeniach, które teoretycznie powinny dać radę. Dobra wiadomość: w większości przypadków da się to zdiagnozować metodycznie i naprawić bez „strzelania” optymalizacjami na ślepo.
W tym artykule dostajesz konkretną checklistę: od ustawienia środowiska, przez pomiary (profile/release), po interpretację trace’ów i typowe naprawy (build/layout/paint, obrazy, listy, animacje, isolate’y). Na końcu dorzucam sekcję „anty-regresja”, żeby performance nie psuł się po każdym sprincie.
0) Ustal, co jest problemem: UI thread vs raster (GPU)
Flutter renderuje klatkę w dwóch głównych etapach:
- UI thread (Dart): build → layout → paint, przygotowanie sceny.
- Raster thread (Skia/GPU): rasteryzacja, rysowanie na ekran.
Jeśli przekroczysz budżet czasu na klatkę, masz jank. Budżet zależy od odświeżania:
- 60 Hz → ~16,6 ms na klatkę
- 90 Hz → ~11,1 ms
- 120 Hz → ~8,3 ms
Wniosek: na 120 Hz nawet „niewinne” 10 ms w UI robi problem. Dlatego diagnozę zaczynasz od odpowiedzi: czy blokuje Dart (UI), czy rysowanie (raster).
1) Zasada #1: mierz w trybie profile / release
Debug jest wolny (asserty, brak optymalizacji, narzędzia). Jeśli diagnozujesz w debug, możesz „naprawiać” problemy, które w release nie istnieją — albo odwrotnie.
- Na urządzeniu:
flutter run --profile - Na Androidzie (docelowo): testy na fizycznym urządzeniu, nie tylko emulator.
- Na iOS: testy na fizycznym urządzeniu, bo symulator ma inną charakterystykę GPU/CPU.
Tip: jeśli performance „czasem” siada, przygotuj scenariusz odtworzenia (np. scroll listy 30 sekund, przejście między ekranami, otwarcie klawiatury) i powtarzaj go identycznie.
2) Najszybsza checklista: 10-minutowy triage
- Uruchom w profile.
- Włącz Performance Overlay (DevTools albo
WidgetsApp.showPerformanceOverlay). - Zobacz, czy czerwone słupki pojawiają się na górze (UI) czy na dole (raster).
- W DevTools → Flutter frames: zidentyfikuj najgorsze klatki (worst frames).
- Wejdź w Timeline i sprawdź, co zajęło czas (build/layout/paint, shader compilation, image decode, GC, itp.).
Po tym kroku zwykle już wiesz, czy idziesz w optymalizację widgetów, obrazów, efektów (blur/clip), czy w async/IO/izolaty.
3) Najczęstszy winowajca #1: zbyt częste przebudowy (build)
Klasyka: jedna zmiana stanu powoduje przebudowę zbyt dużej części drzewa widgetów. Objaw: UI thread przekracza budżet, a w trace widać dużo czasu w build.
Jak to rozpoznać
- DevTools → Rebuild stats (lub obserwacja „repaint rainbow”/„track widget rebuilds”).
- W logice stanu: setState/notifyListeners wywoływane zbyt często (np. w timerze co 16 ms).
- Wielkie widgety „containerowe” przebudowują dzieci, mimo że dzieci się nie zmieniają.
Naprawy (konkret)
- Wyciągnij poddrzewa do osobnych widgetów i ogranicz zakres rebuild.
- Używaj const tam, gdzie się da (to realnie obniża koszty build).
- W Provider/Riverpod/Bloc: selektory (
select/Consumerna małych fragmentach), nie jeden globalny listener. - W listach: nie licz rzeczy w build (np. mapowanie 2000 elementów, sortowanie, filtracje). Przenieś to do precompute/cache.
4) Najczęstszy winowajca #2: kosztowny layout i paint
Nawet jeśli build jest OK, możesz tracić czas na layout/paint (np. skomplikowane drzewo, dużo cieni, clipów, blendów, przeźroczystości).
Sygnały ostrzegawcze
- W trace: dużo czasu w „Layout” lub „Paint”.
- Raster thread również rośnie (złożone rysowanie).
- W UI: dużo
Opacity,ClipRRect,BackdropFilter,ShaderMask.
Naprawy
- Unikaj BackdropFilter na dużych powierzchniach; jeśli musisz, ogranicz region i cache’uj wynik (np. przez stałą warstwę).
- Ostrożnie z clip: clip bywa kosztowny. Jeśli chcesz zaokrąglenia na obrazku, często lepiej przygotować asset z rounded corners albo użyć mniejszego clipa.
- RepaintBoundary — izoluj elementy, które często się animują, żeby nie repaintować całego ekranu.
- Jeśli animujesz tylko transformację/opacity, preferuj Transform/Opacity na małym drzewie zamiast przebudowywania całego layoutu.
5) Listy i scroll: wydajność 90% aplikacji
Jeśli jank pojawia się głównie podczas scrolla, zacznij od list. Błędy są powtarzalne:
Checklista list
- Używaj builderów:
ListView.builder,SliverList,GridView.builder. - Ustaw itemExtent lub prototypeItem, jeśli wysokość elementów jest stała/przewidywalna (mniej pracy dla layoutu).
- Nie wkładaj
ListViewwSingleChildScrollView(chyba że wiesz, co robisz). - W itemach listy unikaj zagnieżdżonych
IntrinsicHeight/IntrinsicWidth— potrafią zabić layout. - Obrazy: cache i rozmiary (patrz sekcja o obrazach).
Typowa poprawka: odchudź item
Item listy powinien być możliwie „płaski”: minimalna liczba warstw, mało efektów. Jeśli masz złożony item (karta + obraz + gradient + kilka linii tekstu + przyciski), rozważ:
- pre-renderowanie stałych elementów
- oddzielenie animacji od reszty przez
RepaintBoundary - wymianę ciężkich efektów na prostsze (np. cień → border)
6) Obrazy: decode, resize, cache, shimmer
Obrazki to częsty powód skoków raster i UI (decode). Zasada: nigdy nie dekoduj ogromnych obrazów, jeśli wyświetlasz je jako miniatury.
Co sprawdzić
- Czy obrazki mają dopasowany rozmiar (np. thumbnail z backendu)?
- Czy używasz
cacheWidth/cacheHeightdlaImage.network/ResizeImage? - Czy shimmer/placeholder nie repaintuje całej listy?
Przykład: wymuszenie dekodowania do docelowego rozmiaru
Image.network(
url,
width: 120,
height: 120,
fit: BoxFit.cover,
cacheWidth: 240, // np. 2x dla ekranów high-dpi
cacheHeight: 240,
)
7) Shader compilation jank (szczególnie na Androidzie)
Czasem aplikacja „przycina” przy pierwszym użyciu konkretnej animacji/efektu. To może być kompilacja shaderów.
- Objaw: jank na „pierwszym razie”, potem już płynnie.
- Trace: wpisy związane z shaderami / Skia.
Rozwiązania zależą od wersji Fluttera i platformy, ale warto rozważyć:
- upgrade Fluttera (wiele fixów jest po stronie engine)
- „rozgrzanie” krytycznych animacji na starcie (ostrożnie, żeby nie opóźnić startu)
- unikanie ekstremalnych efektów (np. złożone maski/blur)
8) GC i alokacje: jank co kilka sekund
Jeśli przycięcia pojawiają się periodycznie, a timeline pokazuje GC, problemem są alokacje i „śmieci” tworzone w gorącej ścieżce (scroll/animacje).
Typowe źródła alokacji
- Tworzenie nowych obiektów w
build(np. listy, mapy, regexy) dla każdego frame. - Formatowanie dat/liczb w trakcie scrolla (np.
intlbez cache). - String concatenation w builderach bez potrzeby.
Co robić
- Cache’uj obliczenia poza build.
- Używaj
consti przenoś stałe do pól statycznych. - Przy drogich obliczeniach: przenieś do isolate (patrz niżej).
9) CPU-heavy: przenieś do isolate albo zoptymalizuj algorytm
Flutter UI thread nie powinien robić ciężkiej roboty (JSON, kompresja, kryptografia, duże sortowania) w trakcie interakcji użytkownika.
Minimalny pattern: compute()
import 'package:flutter/foundation.dart';
Future<Result> parseBigJson(String raw) async {
return compute(_parse, raw);
}
Result _parse(String raw) {
// ciężka praca poza UI
return Result.fromJson(raw);
}
Uwaga: isolate nie jest magicznym rozwiązaniem. Jeśli przesyłasz ogromne obiekty między isolate’ami, koszty kopiowania mogą zjeść zyski. Najlepiej przekazywać surowe dane (string/bytes) i zwracać zwięzły wynik.
10) Animacje: mniej „work per frame”
Animacje w Flutterze mogą być bardzo tanie albo bardzo drogie — zależy, czy animujesz właściwości, które wymagają relayout/repaint dużych obszarów.
Checklista animacji
- Preferuj implicit animations na małych widgetach (
AnimatedOpacity,AnimatedContainer) — ale kontroluj, co dokładnie animują. - W custom animacjach: użyj
AnimatedBuilderi przekazujchild, żeby nie rebuildować stałych elementów. - Unikaj animacji, które zmieniają rozmiar w liście (relayout wielu elementów naraz).
11) „Nie rób tego”: najczęstsze pułapki
- setState w pętli lub co tick timera bez throttlingu.
- IntrinsicHeight w listach.
- Duże blur i maski na pełny ekran.
- Za duże obrazy bez resize/cachingu.
- Debug printing w gorących ścieżkach (logi potrafią robić jank).
12) Anty-regresja: jak nie psuć performance co sprint
Najgorsze, co możesz zrobić, to „naprawić” performance jednorazowo i zostawić bez ochrony. W praktyce działa:
- Profilowanie na koniec sprintu na 1–2 krytycznych flow (scroll, checkout, feed).
- Ustalony budżet: np. „scroll listy 200 elementów bez czerwonych barów na Pixelu X”.
- Review zmian UI pod kątem: listy/obrazy/efekty.
- Wersjonowanie i obserwacja crashy/ANR + metryk startu.
Podsumowanie
Diagnoza jank w Flutterze to głównie proces: mierz w profile, rozdziel UI vs raster, znajdź najgorsze klatki, a potem napraw przyczynę (rebuild, layout/paint, obrazy, shadery, GC, ciężka logika). Jeśli chcesz, możesz potraktować checklistę z tego artykułu jak „playbook” i odpalać ją zawsze, gdy pojawia się problem z płynnością.
FAQ
Czy RepaintBoundary zawsze pomaga?
Nie. Pomaga, gdy masz fragment UI często repaintowany i chcesz odizolować go od reszty. Jeśli problemem jest build albo layout, RepaintBoundary może nic nie dać albo nawet zaszkodzić (więcej warstw do zarządzania).
Czy da się „wymusić” 60 FPS na każdym telefonie?
Nie zawsze. Budżet zależy od hardware’u i odświeżania. Realistyczny cel to: brak stałych przycięć na urządzeniach z Twojej grupy docelowej + brak dramatycznych spike’ów na starszych modelach.
Debug działa płynnie, a release nie — możliwe?
Rzadziej, ale tak. Czasem różnice wynikają z kompilacji shaderów, zachowania GPU, lub optymalizacji, które zmieniają profile alokacji. Dlatego testy na docelowych urządzeniach i w profile/release są kluczowe.
Zobacz też: Jak wygląda integracja płatności w aplikacjach Flutter?
