フラッター パフォーマンス: インペラ、レンダリング パイプライン、ジャンクの除去
ジャンク — アニメーション内の煩わしい途切れ、スクロール内の欠落したフレーム — そしてモバイル ユーザー エクスペリエンスの最大の敵です。 Flutter の歴史的原因 最も一般的だったのは シェーダのコンパイル: 初めての GPU グラフィック効果をレンダリングする必要があったため、シェーダー JIT がコンパイルされ、 フリーズが見える。と インペラ — 新しい Flutter レンダラー、 Flutter 3.10 以降は iOS で、Flutter 3.24 以降は Android 10+ でデフォルトになりました — これ 問題を根本から解決します。
ただし、Impeller によってパフォーマンスの問題がすべて解消されるわけではありません。無駄な再構成 ウィジェット ツリーの、UI スレッドでの負荷の高い操作、最適化されていない画像: これらの問題はレンダラーに関係なく存在します。このガイドではそれについて説明します インペラーのアーキテクチャと、特定および識別するための実用的なツールの両方 アプリのパフォーマンスのボトルネックを解決します。
何を学ぶか
- Impeller vs Skia: 新しいレンダラーの仕組みと、それがシェーダー ジャンクを排除する理由
- Flutter レンダリング パイプライン: UI スレッド、ラスター スレッド、フレーム バジェット
- パフォーマンス オーバーレイ: リアルタイムでグラフを読み取ります
- Flutter DevTools - 遅いフレームを識別するタイムライン プロファイラー
- 無駄な再構築: const、RepaintBoundary、selectContext を削減するための
- 重い画像:cacheWidth、cacheHeight、precacheImage
- 最適化された ListView: itemExtent、prototypeItem、addRepaintBoundaries
インペラー: その仕組みと、Skia よりも優れている理由
スキア (以前のレンダラ) コンパイルされた OpenGL/Vulkan シェーダ 実行時の JIT: エフェクトが初めてレンダリングされたときにコンパイルされました。 シェーダにより、ユーザーに見える 50 ~ 500 ミリ秒のフリーズが発生します。これは Impeller Flutter 以前のアプリの特徴である「最初のフレームのジャンク」。
インペラ コンパイルすることでこれを解決します みんな シェーダー アプリのビルド中のコンパイル時に。実行時には、すべてのシェーダーが すでに準備完了: JIT コンパイルゼロ、最初のフレームのジャンクゼロ。インペラーも使用 より最新のレンダリング パイプライン (iOS では Metal、Android では Vulkan)。
// 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" />
レンダリング パイプライン: UI スレッドとラスター スレッド
Flutter はレンダリングに 2 つのメイン スレッドを使用します。彼らがどのように相互作用するかを理解する パフォーマンスの問題をデバッグするために不可欠です。
// 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: 遅いフレームを特定する
// 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;
}
}
不必要な再構築を減らす
// 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));
リストビューの最適化
// 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);
}
}
改善の測定
// 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;
}
フラッターパフォーマンスチェックリスト
- すべての静的ウィジェットには const を使用します
- RepaintBoundary で重いアニメーションをラップする
- BLoC/Riverpod で select() を使用してきめ細かい再構築を行う
- 項目の高さが固定されている場合は itemExtent を使用した ListView
- リスト内の画像のcacheWidth/cacheHeightを設定します
- デバッグ モードではなく --profile モードでのプロファイル
- リリース前にパフォーマンスオーバーレイでFPSをチェック
結論
Impeller は、Flutter アプリの歴史上最も厄介な問題を解決しました。 ジャンクコンパイルシェーダー。ただし、アプリのパフォーマンスは次の要素にも依存します。 無駄な再構築が何回行われるか、画像はどのように管理されるか そしてウィジェットツリーがどのように構造化されているか。インストゥルメント — パフォーマンス オーバーレイ、 DevTools タイムライン、FrameTiming コールバック — それぞれを識別可能にします サブフレーム精度のボトルネック。







