Performanța flutterului: rotor, conductă de redare și eliminarea Jank
Jank - acele bâlbâieli enervante din animație, acele cadre lipsă din scroll - și inamicul numărul unu al experienței utilizatorului mobil. În Flutter, cauza istorică cel mai comun a fost cel compilare shader: prima dată GPU-ul a trebuit să redeze un efect grafic, a compilat shaderul JIT, provocând a îngheț vizibil. Cu Rotor — noul dispozitiv de redare Flutter, acum implicit pe iOS de la Flutter 3.10 și pe Android 10+ de la Flutter 3.24 - aceasta problema si rezolvata la radacina.
Dar Impeller nu elimină toate problemele de performanță. Reconstrucții inutile al arborelui widget, operațiuni grele pe firul de UI, imagini neoptimizate: aceste probleme există indiferent de redator. Acest ghid o acoperă atât arhitectura Impeller cât şi instrumentele practice de identificare şi rezolvați blocajele de performanță din aplicația dvs.
Ce vei învăța
- Impeller vs Skia: cum funcționează noul renderer și de ce elimină shader jank
- Conducta de randare Flutter: fire de UI, fire raster, bugete de cadre
- Suprapunere de performanță: citiți grafice în timp real
- Flutter DevTools - Profil de cronologie pentru a identifica cadrele lente
- Reconstrucții inutile: const, RepaintBoundary, selectContext pentru a le reduce
- Imagini grele: cacheWidth, cacheHeight, precacheImage
- ListView optimizat: itemExtent, prototypeItem, addRepaintBoundaries
Rotor: Cum funcționează și de ce este mai bun decât Skia
Skia (redarea anterioară) a compilat shadere OpenGL/Vulkan JIT în timpul execuției: prima dată când un efect a fost redat, acesta a fost compilat shader-ul, provocând o înghețare de 50-500 ms vizibilă pentru utilizator. Acesta a fost „first-frame jank” caracteristică aplicațiilor pre-Impeller Flutter.
Rotor rezolvă acest lucru prin compilare toată lumea umbritorii în timpul compilării în timpul construirii aplicației. În timpul execuției, toți shaders-urile sunt deja gata: zero compilație JIT, zero primul cadru jank. Rotor folosește și el o conductă de randare mai modernă (Metal pe iOS, Vulkan pe Android).
// 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" />
Conducta de randare: UI Thread și Raster Thread
Flutter folosește două fire principale pentru randare. Înțelegeți cum interacționează și esențial pentru depanarea problemelor de performanță:
// 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: identificați cadre lente
// 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;
}
}
Reduceți reconstrucțiile inutile
// 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));
Optimizare 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);
}
}
Măsurarea îmbunătățirii
// 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;
}
Lista de verificare a performanței Flutter
- Utilizați const pentru toate widget-urile statice
- Înfășurați animații grele în RepaintBoundary
- Utilizați select() în BLoC/Riverpod pentru reconstrucții granulare
- ListView cu itemExtent dacă articolele au înălțime fixă
- Setați cacheWidth/cacheHeight pentru imaginile din liste
- Profil în modul --profil, nu în modul depanare
- Verificați FPS cu suprapunere de performanță înainte de lansare
Concluzii
Impeller a rezolvat cea mai enervantă problemă istorică a aplicațiilor Flutter: shaderul de compilație jank. Dar și performanța unei aplicații depinde câte reconstrucții inutile au loc, cum sunt gestionate imaginile și cum este structurat arborele widget. Instrumentele — Performance Overlay, Cronologie DevTools, apeluri inverse FrameTiming - fac fiecare identificabil blocaj cu precizie sub-cadru.







