Wydajność trzepotania: wirnik, rurociąg renderujący i eliminacja Jank
Jank — te irytujące zacięcia w animacji, te brakujące klatki w zwoju — i wróg numer jeden w zakresie doświadczeń użytkowników mobilnych. We Flutter, przyczyna historyczna najczęstszym był kompilacja shaderów: pierwszy raz GPU musiał wyrenderować efekt graficzny, skompilował moduł cieniujący JIT, powodując widoczne zamrożenie. Z Wirnik — nowy renderer Flutter, teraz domyślnie na iOS od Flutter 3.10 i na Androidzie 10+ od Flutter 3.24 — to problem i został rozwiązany u źródła.
Ale wirnik nie eliminuje wszystkich problemów z wydajnością. Bezużyteczne rekonstrukcje drzewa widżetów, ciężkie operacje na wątku interfejsu użytkownika, niezoptymalizowane obrazy: problemy te występują niezależnie od modułu renderującego. W tym przewodniku jest to omówione zarówno architekturę wirnika, jak i praktyczne narzędzia do identyfikacji i Rozwiąż wąskie gardła wydajności w swojej aplikacji.
Czego się nauczysz
- Impeller vs Skia: jak działa nowy renderer i dlaczego eliminuje szarpanie shaderów
- Potok renderowania Flutter: wątki interfejsu użytkownika, wątki rastrowe, budżety ramek
- Nakładka wydajności: czytaj wykresy w czasie rzeczywistym
- Flutter DevTools - Profiler osi czasu do identyfikowania wolnych klatek
- Bezużyteczne przebudowy: const, RepaintBoundary,selectContext, aby je zredukować
- Ciężkie obrazy: cacheWidth, cacheHeight, precacheImage
- Zoptymalizowany widok ListView: itemExtent, prototypeItem, addRepaintBoundaries
Wirnik: Jak to działa i dlaczego jest lepszy od Skia
Skia (poprzedni renderer) skompilował shadery OpenGL/Vulkan JIT w czasie wykonywania: Kiedy efekt został wyrenderowany po raz pierwszy, został on skompilowany modułu cieniującego, powodując zamrożenie na 50-500 ms widoczne dla użytkownika. To był „Jank w pierwszej klatce” charakterystyczny dla aplikacji sprzed Impeller Flutter.
Wirnik rozwiązuje ten problem poprzez kompilację wszyscy shadery w czasie kompilacji podczas tworzenia aplikacji. W czasie wykonywania wszystkie shadery są już gotowy: zerowa kompilacja JIT, zero Janck pierwszej klatki. Wirnik również wykorzystuje bardziej nowoczesny potok renderowania (Metal na iOS, Vulkan na Androidzie).
// 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" />
Potok renderowania: wątek interfejsu użytkownika i wątek rastrowy
Flutter wykorzystuje dwa główne wątki do renderowania. Zrozum, jak na siebie oddziałują i niezbędne do debugowania problemów z wydajnością:
// 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: Identyfikuj wolne klatki
// 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;
}
}
Ogranicz niepotrzebne przebudowy
// 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));
Optymalizacja 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);
}
}
Pomiar poprawy
// 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 kontrolna wydajności Flutter
- Użyj const dla wszystkich statycznych widżetów
- Zawijaj ciężkie animacje w RepaintBoundary
- Użyj funkcji Select() w BLoC/Riverpod do szczegółowej rekonstrukcji
- ListView z itemExtent, jeśli elementy mają stałą wysokość
- Ustaw cacheWidth/cacheHeight dla obrazów na listach
- Profil w trybie --profile, a nie w trybie debugowania
- Sprawdź FPS z nakładką wydajności przed wydaniem
Wnioski
Impeller rozwiązał najbardziej irytujący, historyczny problem aplikacji Flutter: moduł cieniujący kompilacji Jank. Ale wydajność aplikacji zależy również od ile ma miejsce bezużytecznych rekonstrukcji, jak zarządza się obrazami oraz strukturę drzewa widżetów. Instrumenty — Nakładka Performance, Oś czasu DevTools, wywołania zwrotne FrameTiming — umożliwiają identyfikację każdego z nich wąskie gardło związane z precyzją ramy pomocniczej.







