Mobilni razvoj
FlutterRazvoj mobilnih aplikacijaPerformanseProfiliranjeDevToolsOptimizacija

Optimizacija performansi u Flutteru: kako održavamo aplikacije na 60 fps (profiliranje + popravci)

AO
Adrijan Omičević
·14 min čitanja

# Što ćeš naučiti#

Ovaj vodič donosi ponovljiv workflow za optimizaciju performansi u Flutteru koji koristimo u produkciji kako bismo tipične interakcije držali na 60 fps na uređajima srednje klase.

Naučit ćeš kako profilirati jank uz Flutter DevTools, prepoznati je li usko grlo rad na UI threadu ili rad na raster threadu, te primijeniti popravke u četiri područja s najvećim učinkom: smanjenje rebuildova, renderiranje, slike, i isolates.

Također ćemo definirati praktične performance budžete za česte ekrane i uključiti kontrolne liste prije i poslije koje možeš ponoviti za svaku značajku.

# Zašto je 60 fps važno i što je zapravo “jank”#

Na uređaju od 60 Hz imaš oko 16,67 milisekundi za izgradnju i renderiranje svakog framea. Ako UI posao ili rasterizacija prijeđu taj budžet, frameovi promaše svoj rok i korisnici vide trzanje.

Na 120 Hz uređajima budžet je oko 8,33 milisekundi, pa aplikacija koja je “skroz OK” na 60 Hz može djelovati tromo na modernim mobitelima.

ℹ️ Napomena: Mnogi “problemi s performansama” su zapravo problemi konzistentnosti. Ekran koji u prosjeku traje 10 ms, ali svakih nekoliko sekundi skoči na 40 ms, i dalje djeluje loše jer ljudi nepravilno kretanje primjećuju više nego same prosjeke.

# Performance budžeti koje koristimo za česte ekrane#

Budžeti pomažu da ne optimiziraš nasumično. Odabereš klasu ciljnih uređaja i definiraš što znači “dovoljno dobro” po interakciji.

Ekran ili interakcijaCiljani FPSBudžet UI threadaBudžet rasteraBilješke
Pokretanje aplikacije do prvog interaktivnog framea60manje od 16,67 ms po frameumanje od 16,67 msMjeri odvojeno od vremena cold starta
Scroll home liste sa slikama60manje od 10 msmanje od 12 msOstavi prostora za GC i input
Product feed s animacijama60manje od 8 msmanje od 10 msTeške animacije povećavaju rizik
Tipkanje u pretrazi s live prijedlozima60manje od 6 msmanje od 10 msUI mora ostati responzivan
Pomicanje (pan) na mapi60manje od 8 msmanje od 8 msRaster često dominira
Otvaranje i zatvaranje modala60manje od 8 msmanje od 8 msOvdje se jank jako primijeti

Ako ciljaš 120 Hz uređaje, prepolovi ove budžete. U praksi ciljamo na headroom kako bi aplikacija ostala stabilna kad OS raspoređuje pozadinski posao.

🎯 Ključna poruka: Ako ne možeš jasno izreći budžet za interakciju koju optimiziraš, vjerojatno ćeš trošiti vrijeme na promjene malog učinka.

# Preduvjeti#

ZahtjevVerzijaBilješke
Flutter3.19+Preporučen stable
DartBundledUskladi s Flutter SDK-om
Flutter DevToolsNajnovijiKoristi DevTools koji dolazi uz Flutter
UređajFizički Android i iOSUređaji srednje klase otkrivaju realna uska grla
Build modoviProfile i ReleaseDebug nije dobar za timing

Pomoći će i čista baseline arhitektura. Ako ti je UI čvrsto vezan uz dohvat podataka i poslovnu logiku, kontrola rebuildova postaje teža. Ako trebaš pomoć oko strukture, vidi Arhitektura Flutter aplikacije: Clean Architecture vs Feature-first i naše smjernice za skalabilne state patternse u Flutter upravljanje stanjem u 2026..

# Ponovljiv workflow: dijagnosticiraj, dokaži, popravi, provjeri#

Ovo je workflow koji koristimo kako bismo izbjegli “cargo cult” optimizacije.

Korak 1: Reproduciraj i snimi minimalni scenarij#

Odaberi jednu interakciju koja trzucka: brzi scroll, otvaranje modala, prebacivanje tabova, tipkanje u pretrazi, širenje stavke liste.

Definiraj reproducibilan skript:

  1. 1
    Otvori ekran
  2. 2
    Izvedi interakciju 10 do 15 sekundi
  3. 3
    Stani
  4. 4
    Ponovi dvaput da potvrdiš konzistentnost

Koristi stvarne uređaje. Emulatore često “sakriju” GPU i termalno ponašanje, a iOS simulator nije reprezentativan.

Korak 2: Uključi pravu observability#

Uključi ove alate prvo, prije promjena koda.

  1. 1
    Flutter performance overlay
  2. 2
    DevTools Performance timeline
  3. 3
    DevTools CPU profiler kad treba
  4. 4
    Rebuild tracking kad sumnjaš na widget churn
Dart
// main.dart
import 'package:flutter/material.dart';
 
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}
 
// Enable during profiling only.
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showPerformanceOverlay: false, // set true during local profiling
      home: const HomeScreen(),
    );
  }
}

💡 Savjet: Drži poseban “profiling flavor” s uključenim logiranjem i overlayima. Nemoj ga miješati u produkcijske buildove i nemoj donositi zaključke o performansama na temelju debug-only flagova.

Korak 3: Snimi trace u DevTools Performance#

Pokreni u profile modu:

Bash
flutter run --profile

Otvori DevTools, idi na Performance, klikni Record, izvedi interakciju, zatim zaustavi.

Sada klasificiraj jank:

  • UI jank: UI thread promašuje rokove za frameove jer gradi layoute, vrti težak Dart kod ili uzrokuje čest garbage collection.
  • Raster jank: GPU thread ne stiže zbog skupih shadera, clippanja, blura, velikih slika ili previše layera.

Korak 4: Pronađi hotspot i njegovu kategoriju#

Koristi ove signale:

  • Flutter Frames graf: pokazuje koji frameovi su promašeni.
  • Timeline eventi: traži dugačke “Build”, “Layout”, “Paint”, “Rasterize”.
  • CPU profiler: provjeri Dart hot pathove kad je UI thread spor.
  • Rebuild stats: pronađi widgete koji se rebuildaju prečesto.

Cilj je odgovoriti na jedno pitanje: koji je najskuplji posao tijekom promašenih frameova i zašto se događa?

Korak 5: Primijeni jedan fix odjednom, zatim ponovno izmjeri#

Napravi jednu promjenu, ponovno pokreni isti skript, snimi novi trace i usporedi.

Ako se traceovi ne poboljšaju, vrati promjenu. Performance rad je iterativan i vođen dokazima.

# Dijagnosticiranje janka u DevTools: na što gledati#

Tumačenje performance overlaya#

Overlay prikazuje dva grafa:

  • Gornji graf: posao na UI threadu
  • Donji graf: posao na raster threadu

Ako gornji graf skače, sumnjaj na rebuildove, layout ili sinkroni compute na glavnom isolateu. Ako donji graf skače, sumnjaj na složenost renderiranja i slike.

Čitanje timeline tracea kao kontrolne liste#

U snimljenom timelineu:

  1. 1
    Pronađi janky frame (preko budžeta).
  2. 2
    Zoomiraj.
  3. 3
    Provjeri koja faza dominira:
    • Build i Layout znači widget churn ili složene layoute.
    • Paint znači teško custom paintanje ili efekte.
    • Raster znači shader efekte, clipove, skaliranje slika ili previše layera.
  4. 4
    Identificiraj ponavljajuće obrasce kroz više janky frameova.

Kada koristiti CPU profiler#

Ako je UI thread spor, ali timeline ne pokazuje jasno zašto, pokreni CPU profiler i ponovi interakciju. Traži:

  • Teško JSON dekodiranje
  • Skupi string operacije
  • Sortiranje velikih lista
  • Diffanje i mapiranje velikih kolekcija pri svakom frameu
  • Sinkroni file IO ili velika čitanja preferenci

# Kategorija popravaka 1: Smanjenje rebuildova (najveći ROI)#

Pretjerani rebuildovi su jedan od najčešćih uzroka janka u Flutter aplikacijama jer povećavaju i UI posao i posljedični layout i paint.

Obrasci simptoma#

  • Scroll liste pokreće rebuildove nepovezanih widgeta poput app bara i footera.
  • Tipkanje u search boxu uzrokuje rebuild cijele stranice.
  • Promjena jedne stavke u listi rebuilda sve list tileove.

Fix 1: Smanji opseg rebuilda razbijanjem widgeta#

Ako je sve u jednom velikom build metodu, Flutter ne može izolirati što se treba rebuildati.

Dart
// Bad: big build method reacts to small state changes.
@override
Widget build(BuildContext context) {
  final state = context.watch<HomeState>();
  return Column(
    children: [
      Header(user: state.user),
      SearchBar(query: state.query),
      Expanded(child: ResultsList(items: state.items)),
      Footer(version: state.version),
    ],
  );
}

Razbij i selektiraj samo ono što svaki sub-widget treba.

Dart
// Better: smaller rebuild surfaces via selectors.
class HeaderSection extends StatelessWidget {
  const HeaderSection({super.key});
 
  @override
  Widget build(BuildContext context) {
    final userName = context.select<HomeState, String>((s) => s.user.name);
    return Header(userName: userName);
  }
}

Ovaj pristup radi s više state rješenja, ali princip je isti: pretplati se na najmanji potrebni “slice”.

Fix 2: Preferiraj const i stabilna widget podstabla#

Const widgeti smanjuju trošak rebuilda i object churn. Stabilna podstabla mogu izbjeći i relayout.

Dart
return const Padding(
  padding: EdgeInsets.all(16),
  child: Text('Settings'),
);

Također izbjegavaj stvaranje novih objekata u buildu kad se ne moraju mijenjati, posebno TextStyle, BorderRadius, EdgeInsets i Duration.

Fix 3: Ne rebuildaj liste kada se mijenja jedna stavka#

Kod lista ažuriraj stavku, ne listu. Uobičajene tehnike:

  • Koristi keyed stavke liste kako bi Flutter sačuvao state elemenata.
  • Koristi granularni state po retku kad je moguće.
  • Izbjegavaj setState na razini stranice za promjene po retku.
Dart
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    final item = items[index];
    return KeyedSubtree(
      key: ValueKey(item.id),
      child: ProductTile(item: item),
    );
  },
);

Fix 4: Cacheiraj izvedene podatke, ne računaj ponovno pri svakom frameu#

Ako filtrirane liste, grupirane sekcije ili formatirane stringove računaš u build, plaćaš taj trošak pri svakom rebuildu.

Premjesti to u:

  • Memoizirane selectore
  • View-model computed fieldove
  • Jednokratni pre-processing korak kad se podaci promijene

Ovdje arhitektura radi razliku. Predvidljiv state layer olakšava da izračunaš jednom i ponovno koristiš. Ako ti je state layer kaotičan, vrati se na Flutter upravljanje stanjem u 2026..

⚠️ Upozorenje: Nemoj “optimizirati” dodavanjem globalnih cacheova posvuda. Većina regresija performansi dolazi od neograničenog cacheiranja, zastarjelih podataka i kompleksnosti koja onemogućuje buduće popravke.

# Kategorija popravaka 2: Renderiranje i layout (kada Raster ili Paint skaču)#

Ako raster thread skače, optimizacija rebuildova sama po sebi neće pomoći. Trebaš smanjiti trošak paintanja.

Česti uzroci visokog render troška#

UzrokZašto je skupoTipičan fix
Backdrop blurTraži offscreen renderiranjeUkloni blur ili ograniči područje blura
Pretjerano clippingSprječava određene GPU optimizacijeClipaj samo gdje je nužno
Velike sjeneDodatni blur passoviLakše sjene, manji spread
Ugniježđeni opacityForsira offscreen layereIzbjegavaj opacity na velikim podstablima
OverdrawPaintanje piksela više putaPojednostavi pozadine i layere

Fix 1: Koristi RepaintBoundary tamo gdje stvarno pomaže#

RepaintBoundary izolira repaint regije. Koristi ga kad mali animirani dio uzrokuje repaint velike statične površine.

Dobri kandidati:

  • Animirani brojači unutar statične kartice
  • Mali indikatori napretka u velikoj stavci liste
  • Video thumbnaili s overlay animacijama

Loši kandidati:

  • Omotavanje svega, što povećava broj layera i potrošnju memorije
Dart
RepaintBoundary(
  child: AnimatedBuilder(
    animation: animation,
    builder: (context, _) => Transform.scale(
      scale: animation.value,
      child: const Icon(Icons.favorite),
    ),
  ),
);

Fix 2: Izbjegni “layout thrash” zbog intrinsic mjerenja#

Widgeti koji se oslanjaju na intrinsic sizing mogu uzrokovati dodatne layout passove. Ako u traceovima vidiš ponavljani layout, pregledaj upotrebu intrinsic patterna i zamijeni ih eksplicitnim constraintovima.

Preferiraj SizedBox, ConstrainedBox i jasno definirane layoute.

Fix 3: Drži scroll hijerarhije jednostavnima#

Ugniježđeni scrollables i složeni sliveri mogu biti ispravni, ali ih je lako učiniti skupima.

Praktične smjernice:

  • Preferiraj jedan primarni scrollable po ekranu.
  • Koristi ListView.builder ili slivere s lazy buildingom.
  • Izbjegavaj shrinkWrap: true u velikim listama osim ako moraš, jer može forsirati kompletan layout.

# Kategorija popravaka 3: Slike (tihi ubojica performansi)#

Slike mogu uzrokovati i UI i raster jank zbog dekodiranja, resizeanja i overdrawa.

Budžeti za slike koji sprječavaju većinu problema#

ScenarijPreporučena max display veličinaPreporučena veličina datotekeBilješke
Thumbnail u listi64 do 120 logical pixela10 do 30 KBKoristi WebP ili AVIF kad je moguće
Hero slika na kartici300 do 500 logical pixela širine50 do 150 KBCacheiraj i prefetchaj
Full-screen slikaU skladu sa širinom uređaja150 do 400 KBProgressive loading pomaže

Ako dekodiraš 4000 x 3000 JPEG za thumbnail 100 x 100, platit ćeš to u memoriji i vremenu.

Fix 1: Dekodiraj na ciljanu veličinu#

Koristi cacheWidth i cacheHeight kako bi Flutter dekodirao približno na display veličinu.

Dart
Image.network(
  url,
  width: 96,
  height: 96,
  fit: BoxFit.cover,
  cacheWidth: 192,  // roughly 2x for high density screens
  cacheHeight: 192,
);

Fix 2: Precacheaj strateški za scroll#

Ako se lista scrolla u slike koje se istovremeno dekodiraju, dobiješ trzanje. Precacheaj sljedećih nekoliko slika kada imaš idle vrijeme.

Dart
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  for (final url in urls.take(5)) {
    precacheImage(NetworkImage(url), context);
  }
}

Fix 3: Smanji overdraw u karticama s puno slika#

Izbjegavaj slaganje više poluprozirnih gradijenata, blurova i sjena preko velikih slika. Ako dizajn to zahtijeva, ograniči područje efekta.

# Kategorija popravaka 4: Isolates i pozadinski posao (kada CPU skače)#

Ako timeline pokazuje dugačke Dart zadatke, vjerojatno blokiraš glavni isolate.

Koristi isolates za:

  • Parsiranje velikih JSON payloadova
  • Transformaciju podataka na velikim listama
  • Enkripciju i hashing
  • Generiranje PDF-ova ili thumbnaila

Praktično pravilo#

Ako sinkroni zadatak traje više od oko 4 do 8 milisekundi na ciljnom uređaju srednje klase, tretiraj ga kao kandidata za prebacivanje u isolate.

Primjer: Parsiranje JSON-a u isolateu pomoću compute#

Dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
 
List<Map<String, dynamic>> parseItems(String body) {
  final decoded = jsonDecode(body) as List<dynamic>;
  return decoded.cast<Map<String, dynamic>>();
}
 
Future<List<Map<String, dynamic>>> parseItemsAsync(String body) {
  return compute(parseItems, body);
}

Ovo ostavlja UI isolate slobodnim za renderiranje. Upari s cacheiranjem da ne parsiraš isti payload više puta.

Ne prebacuj sve s glavnog isolatea#

Isolates dodaju overhead i kompleksnost. Također traže message passing i kopiranje, što može biti skupo za velike objekte.

Koristi isolates za težak posao, ne kao default.

# Prije i poslije: kontrolna lista koju možeš ponoviti za svaku značajku#

Ovo je razlika između “optimizirali smo jednom” i “održavamo aplikaciju brzom”.

Kontrolna lista prije profiliranja#

ProvjeraCiljKako provjeriti
Profile mode na uređajuDaflutter run --profile
Definiran repro skriptDaIsti koraci pri svakom runu
Definirani budžetiDaKoristi tablicu u ovom postu
Minimalno logiranjaDaIzbjegni spam u hot pathovima
Realistične slikeDaTestiraj s produkcijski sličnim medijima

Kontrolna lista nakon popravaka#

ProvjeraCiljKako provjeriti
Manje promašenih frameovaDaDevTools Performance frames graf
Headroom za UI i rasterNajmanje 20 postoOverlay grafovi ostaju ispod budžeta
Manji broj rebuildovaDaRebuild stats i widget inspector
Nema novih memory spikeovaDaDevTools Memory i GC frekvencija
Potvrđeno u releaseuDaflutter run --release na uređaju

💡 Savjet: Drži screenshotove “prije” i “poslije” traceova u opisu PR-a. Time performance rad postaje reviewable i sprječava da se regresije kasnije ponovno uvedu.

# Kako performance rad utječe na trošak i isporuku#

Optimizacija performansi nije besplatna, ali je predvidljiva kada budžete uvedeš rano. Popravljanje performansi nakon što su featurei već isporučeni obično košta više jer UI, state i data slojevi postaju teži za refaktoriranje.

Ako planiraš novu aplikaciju, planiranje vremena za profiliranje i performance gateove rano može smanjiti rework i rizik store reviewa. Za način razmišljanja o troškovima vidi Trošak razvoja Flutter aplikacije.

# Ključne poruke#

  • Profiliraj u profile modu na stvarnim uređajima, snimi DevTools traceove i klasificiraj jank kao UI-thread ili raster-thread prije nego diraš kod.
  • Najbrže smanjuješ jank tako da smanjiš opseg rebuilda: razbijanje widgeta, selektori, stabilna podstabla i const konstruktori.
  • Kad raster skače, optimiziraj složenost renderiranja: ograniči blur, clipping, opacity layere i koristi RepaintBoundary samo za ciljanu izolaciju.
  • Slike tretiraj kao pipeline: dekodiraj na ciljanu veličinu uz cacheWidth i cacheHeight, prefetchaj strateški i izbjegavaj skupe overlaye.
  • Prebaci teške CPU zadatke u isolates kada sinkroni posao prelazi oko 4 do 8 milisekundi na ciljnom uređaju, pa potvrdi s “prije i poslije” traceovima.

# Zaključak#

Optimizacija performansi u Flutteru je najlakša kada je workflow, a ne jednokratni sprint: postavi budžete, reproduciraj jank, snimi traceove, primijeni jedan fix i provjeri u release buildovima.

Ako želiš da auditiramo najsporije ekrane tvoje aplikacije, definiramo performance budžete za tvoj proizvod i implementiramo popravke bez destabilizacije arhitekture, javi se Samiodi. Gradimo Flutter aplikacije koje ostaju fluidne kako rastu, a promjene potkrepljujemo mjerljivim “prije i poslije” traceovima.

FAQ

Share
A
Adrijan OmičevićSamioda Team
All articles →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.