Flutter Performance: Oběžné kolo, renderovací potrubí a Jank Elimination
Ten šmejd – ty otravné koktání v animaci, ty chybějící snímky ve svitku – a nepřítel číslo jedna pro mobilní uživatelskou zkušenost. Ve Flutterovi historická příčina nejběžnější byl kompilace shaderů: poprvé GPU musel vykreslit grafický efekt, zkompiloval shader JIT, což způsobilo a zamrznutí viditelné. S Oběžné kolo — nový vykreslovací modul Flutter, nyní výchozí pro iOS od Flutter 3.10 a pro Android 10+ od Flutter 3.24 — toto problém a vyřešen u kořene.
Ale Impeller neodstraní všechny problémy s výkonem. Zbytečné rekonstrukce stromu widgetů, náročné operace na vláknu uživatelského rozhraní, neoptimalizované obrázky: tyto problémy existují bez ohledu na renderer. Tento průvodce to pokrývá jak architektura Impelleru, tak praktické nástroje k identifikaci a vyřešit problémová místa výkonu ve vaší aplikaci.
Co se naučíte
- Impeller vs Skia: jak funguje nový renderer a proč eliminuje shader jank
- Vykreslovací kanál Flutter: vlákna uživatelského rozhraní, rastrová vlákna, rozpočty snímků
- Performance Overlay: Čtení grafů v reálném čase
- Flutter DevTools - Profiler časové osy k identifikaci pomalých snímků
- Zbytečné přestavby: const, RepaintBoundary, selectContext je zredukujte
- Těžké obrázky: cacheWidth, cacheHeight, precacheImage
- Optimalizovaný ListView: itemExtent, prototypeItem, addRepaintBoundaries
Oběžné kolo: Jak to funguje a proč je lepší než Skia
Skia (předchozí renderer) zkompiloval OpenGL/Vulkan shadery JIT za běhu: Při prvním vykreslení efektu se zkompiloval shader, což způsobí zamrznutí na 50-500 ms viditelné pro uživatele. Toto bylo "first-frame jank" charakteristické pro aplikace před Impeller Flutter.
Oběžné kolo řeší to kompilací každý shadery v době kompilace během sestavování aplikace. Za běhu jsou všechny shadery již připraveno: nulová kompilace JIT, nulové škubání prvního snímku. Oběžné kolo používá také modernější renderovací kanál (Metal na iOS, Vulkan na Androidu).
// Verifica se Impeller e attivo nella tua app
// Option 1: controlla in runtime
import 'package:flutter/foundation.dart';
void checkRenderer() {
// Solo in debug/profile mode
if (kDebugMode || kProfileMode) {
debugPrint('Flutter renderer: ${FlutterView.rendererInfo}');
}
}
// Option 2: flutter run con flag esplicito
// flutter run --enable-impeller (forza Impeller)
// flutter run --disable-impeller (forza Skia, per confronto)
// Option 3: controlla nel pubspec o AndroidManifest
// Per iOS: Impeller e ON per default (Flutter 3.10+)
// Per Android: ON per default su Android 10+ (Flutter 3.24+)
// Per Android < API 29: ancora Skia (Impeller richiede Vulkan 1.1)
// android/app/src/main/AndroidManifest.xml
// Per disabilitare Impeller su Android (debugging):
// <meta-data
// android:name="io.flutter.embedding.android.EnableImpeller"
// android:value="false" />
Vykreslovací kanál: vlákno uživatelského rozhraní a rastrové vlákno
Flutter používá pro vykreslování dvě hlavní vlákna. Pochopte, jak se vzájemně ovlivňují a nezbytné pro ladění problémů s výkonem:
// Due thread, un obiettivo: 60fps (16ms per frame) o 120fps (8ms per frame)
// UI Thread (Dart main isolate):
// - Esegue il tuo codice Dart
// - Gestisce gestures, layout, build() dei widget
// - Crea i "layer trees" da inviare al raster thread
// Budget: metà del frame budget (8ms per 60fps)
// Raster Thread (C++ renderer):
// - Prende il layer tree dal UI thread
// - Renderizza su GPU (Impeller o Skia)
// - Invia il frame completato alla GPU
// Budget: l'altra metà del frame budget (8ms per 60fps)
// Se uno dei due thread supera il suo budget:
// - UI thread lento: build() troppo pesante, layout complesso
// - Raster thread lento: shader compilation (Skia), clipping complesso, immagini grandi
// Performance Overlay: abilita in DevTools o nel codice
MaterialApp(
showPerformanceOverlay: true, // Mostra i grafici in app
// ...
)
// I due grafici:
// - Grafico superiore: UI thread (rosso = frame slow)
// - Grafico inferiore: GPU/Raster thread (rosso = frame slow)
// La linea verde = 16ms (60fps budget)
// Ogni barra sopra la linea verde = frame dropped
Flutter DevTools: Identifikujte pomalé snímky
// Come usare Flutter DevTools per il profiling
// 1. Avvia in profile mode (non debug: ottimizzazioni attive)
// flutter run --profile
// 2. Apri DevTools
// flutter pub global activate devtools
// flutter pub global run devtools
// 3. Tab "Performance" > Timeline
// Cosa cercare nel timeline:
// - Frame droppati: barre rosse/gialle nella overview
// - Clic su un frame lento per vedere breakdown
// - UI thread: quali widget hanno build() lento?
// - Raster thread: quali layer causano overdraw?
// Strumento "Widget Rebuild Stats" in DevTools:
// Mostra quante volte ogni widget e stato ricostruito
// Cerca widget con rebuild count elevato senza motivo
// Identificare rebuild inutili nel codice:
// Aggiungi questo in debug mode:
class DebugBuildCounter extends StatelessWidget {
final Widget child;
const DebugBuildCounter({required this.child, super.key});
@override
Widget build(BuildContext context) {
debugPrint('BUILD: ${child.runtimeType} at ${DateTime.now().millisecondsSinceEpoch}');
return child;
}
}
Omezte zbytečné přestavby
// 1. const: il widget piu efficiente possibile
// SBAGLIATO: ricreato ad ogni rebuild del parent
Text('Hello World')
// CORRETTO: const = immutabile, mai ricostruito
const Text('Hello World')
// Usa const il piu possibile:
const SizedBox(height: 16)
const Divider()
const Icon(Icons.star)
// 2. RepaintBoundary: isola porzioni del widget tree
// Senza RepaintBoundary: tutta la pagina viene ridisegnata
// quando l'animazione aggiorna il contatore
class AnimatedCounterPage extends StatefulWidget { ... }
// Con RepaintBoundary: solo il contatore viene ridisegnato
class AnimatedCounterPage extends StatefulWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Contenuto statico: non viene ridisegnato
const PageHeader(),
const PageContent(),
// Solo questa parte e re-painted ogni frame
RepaintBoundary(
child: AnimatedCounter(),
),
],
);
}
}
// 3. select() con BLoC/Riverpod: rebuild solo per i dati che cambiano
// SBAGLIATO: il widget si ricostruisce per qualsiasi cambiamento dello UserState
BlocBuilder<UserBloc, UserState>(
builder: (context, state) => Text(state.name),
)
// CORRETTO: rebuild solo quando state.name cambia
BlocSelector<UserBloc, UserState, String>(
selector: (state) => state.name,
builder: (context, name) => Text(name),
)
// Con Riverpod:
// SBAGLIATO
ref.watch(userProvider);
// CORRETTO: rebuild solo per la proprieta name
ref.watch(userProvider.select((user) => user.name));
Optimalizace ListView
// ListView.builder: non costruisce mai widget fuori dalla viewport
// Ma puoi ottimizzarlo ulteriormente:
// 1. itemExtent: se tutti gli elementi hanno la stessa altezza
// Flutter salta il layout e usa solo l'aritmetica per posizionare gli elementi
ListView.builder(
itemExtent: 72.0, // Altezza fissa in pixel
itemCount: items.length,
itemBuilder: (context, index) => ItemTile(item: items[index]),
)
// 2. prototypeItem: per altezze uguali ma non conosciute a priori
ListView.builder(
prototypeItem: const ItemTile(item: Item.empty()),
itemCount: items.length,
itemBuilder: (context, index) => ItemTile(item: items[index]),
)
// 3. addRepaintBoundaries: abilita automaticamente (default: true)
// Mette ogni elemento in una RepaintBoundary separata
// Performance win per liste con elementi che si animano
// 4. Immagini nella lista: usa cacheWidth e cacheHeight
// Per evitare di decodificare immagini piu grandi del necessario
ListView.builder(
itemBuilder: (context, index) => Image.network(
items[index].imageUrl,
cacheWidth: 150, // Decodifica a 150px invece che alla dimensione originale
cacheHeight: 150,
),
)
// 5. precacheImage: pre-carica le immagini prima che siano visibili
// Utile per le prime N immagini della lista
@override
void initState() {
super.initState();
// Pre-carica le prime 10 immagini
for (final item in widget.items.take(10)) {
precacheImage(NetworkImage(item.imageUrl), context);
}
}
Zlepšení měření
// flutter_frames_package: misura FPS in produzione
// Oppure usa PerformanceMonitor manuale
class FrameMonitor extends StatefulWidget {
final Widget child;
const FrameMonitor({required this.child, super.key});
@override
State<FrameMonitor> createState() => _FrameMonitorState();
}
class _FrameMonitorState extends State<FrameMonitor> {
int _droppedFrames = 0;
DateTime? _lastFrame;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addTimingsCallback(_onTimings);
}
void _onTimings(List<FrameTiming> timings) {
for (final timing in timings) {
final frameDuration = timing.totalSpan.inMilliseconds;
if (frameDuration > 16) {
setState(() => _droppedFrames++);
debugPrint('Dropped frame: ${frameDuration}ms');
}
}
}
@override
void dispose() {
WidgetsBinding.instance.removeTimingsCallback(_onTimings);
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}
Kontrolní seznam výkonu flutteru
- Použijte const pro všechny statické widgety
- Zabalte těžké animace do RepaintBoundary
- Použijte select() v BLoC/Riverpod pro granulární přestavby
- ListView s itemExtent, pokud mají položky pevnou výšku
- Nastavte cacheWidth/cacheHeight pro obrázky v seznamech
- Profil v režimu --profile, nikoli v režimu ladění
- Před vydáním zkontrolujte FPS s překrytím výkonu
Závěry
Impeller vyřešil nejnepříjemnější historický problém aplikací Flutter: shader kompilace jank. Ale výkon aplikace také závisí na kolik zbytečných rekonstrukcí probíhá, jak jsou snímky spravovány a jak je strukturován strom widgetů. Nástroje — Performance Overlay, Časová osa DevTools, zpětná volání FrameTiming – každý z nich je identifikovatelný úzké místo s přesností pomocného rámu.







