Flutter: パフォーマンス最適化
パフォーマンスはユーザー体験の基盤 — 60fpsは約束、59fpsは失敗です。
📋 前提条件: 以下を先にマスターしている必要があります
- レッスン10:リストとスクロール
- レッスン12:状態管理 — Riverpod
1. このレッスンで学ぶこと
- Flutter DevTools:Performance Overlay / Widget Rebuild Tracker / CPU Profiler / Memory Chart
- レンダリング最適化:constコンストラクタ / RepaintBoundary / ListView.builder遅延読み込み / 大規模ウィジェットリビルドの回避
- メモリ最適化:画像キャッシュ戦略 / AutomaticKeepAliveClientMixin / Disposeリソース解放
- ネットワーク最適化:データページネーション / 画像CDN / cached_network_image / プリロード
- ShopApp:100万商品リストの60fpsスクロール最適化
2. カクつきのリアルなストーリー
(1) 悩み:商品リストのスクロールが30fps
BobのShopApp商品リストは1000アイテムでスムーズにスクロールしますが、10000アイテムで30fpsに低下。DevToolsで判明:親ウィジェットのsetStateがサブツリー全体をリビルドするため、スクロール中に各商品カードがリビルドをトリガー。さらに、画像にキャッシュがなく — アイテムがビューポートに入るたびに再ダウンロードされ、メモリ使用量が500MBに急増。
(2) ソリューション:的を絞った最適化
パフォーマンス最適化は「すべてを最適化する」ことではなく — ボトルネックを見つけて的を絞ることです。DevToolsが問題を特定 → RepaintBoundaryが分離 → constがリビルドを削減 → 画像キャッシュがネットワーク呼び出しを削減。
DART
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependency: cached_network_image: ^3.0.0 (add to dependencies)
// Product comes from the ShopApp project, full definition in Lesson 26
// Key optimizations
const ProductCard(...); // 1. const avoids rebuild
RepaintBoundary(child: Image.network(...)); // 2. Isolate repaint
CachedNetworkImage(imageUrl: ...); // 3. Cache images
AutomaticKeepAliveClientMixin; // 4. Keep tab state
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) 成果:100万件の60fps + メモリ半減
的を絞った最適化後、Bobの100万商品リストは安定した60fpsを維持、メモリは500MBから200MBに低下、画像キャッシュヒット率は95%に到達。
3. パフォーマンス最適化概要
graph TD
PO[Performance Optimization] --> DEV[DevTools]
PO --> RO[Render Optimization]
PO --> MO[Memory Optimization]
PO --> NO[Network Optimization]
RO --> |const| CW[const Widgets]
RO --> |RepaintBoundary| RB2[Isolate Repaint]
MO --> |cache| IMG[Image Cache]
NO --> |pagination| PG[Pagination API]
DEV --> |Overlay| FPS[FPS Monitor]
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
4. DevToolsパフォーマンス分析
(1) コアDevTools
| ツール | 目的 | 使用方法 |
|---|---|---|
| Performance Overlay | リアルタイムFPS + GPUスレッド時間 | debugShowPerformanceOverlay: true |
| Widget Rebuild Tracker | 頻繁にリビルドされるウィジェットを追跡 | DevTools → Rebuild Tracker |
| CPU Profiler | CPUホットスポットを分析 | DevTools → CPU Profiler |
| Memory Chart | メモリ割り当てとGC | DevTools → Memory |
| Network Inspector | ネットワークリクエストウォーターフォール | DevTools → Network |
| Flutter Inspector | ウィジェットツリーの可視化 | DevTools → Inspector |
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:パフォーマンス分析の有効化
DART
import 'package:flutter/material.dart';
// Development mode: enable performance overlay
MaterialApp(
debugShowPerformanceOverlay: true,
showSemanticsDebugger: false,
// ...
)
// Profile mode: accurate performance measurement
// Run: flutter run --profile
// Then open DevTools: flutter pub global run devtools
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
| 実行モード | 最適化 | 目的 |
|---|---|---|
| Debug | 最適化なし | 開発とデバッグ |
| Profile | 本番に近い | パフォーマンス分析 |
| Release | 完全最適化 | 本番リリース |
5. レンダリング最適化
(1) constコンストラクタ
| 比較 | 非const | const |
|---|---|---|
| リビルド動作 | 毎回新しいインスタンスを生成 | 同じインスタンスを再利用 |
| canUpdate | 通過しない場合あり | 常に通過 |
| パフォーマンスへの影響 | 繰り返しリビルドのオーバーヘッド | リビルドオーバーヘッドゼロ |
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:const最適化された商品カード
DART
import 'package:flutter/material.dart';
// BAD: not const, rebuilds every time
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(8.0), // New instance each build
child: Text('Price: \$${product.price}'),
);
}
// GOOD: const where possible
Widget build(BuildContext context) {
return const Padding( // const padding
padding: EdgeInsets.all(8.0), // Compile-time constant
child: Text('Fixed text'), // const if content is constant
);
}
// For dynamic content, const the wrapper
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: _DynamicPrice(product: product), // Can't const dynamic
);
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:RepaintBoundaryによる再描画の分離
DART
import 'package:flutter/material.dart';
// Product comes from the ShopApp project, full definition in Lesson 26
// Without RepaintBoundary: entire list repaints when one item changes
ListView.builder(
itemCount: 1000,
itemBuilder: (_, i) => ProductCard(product: products[i]),
)
// With RepaintBoundary: only changed card repaints
ListView.builder(
itemCount: 1000,
itemBuilder: (_, i) => RepaintBoundary(
key: ValueKey(products[i].id),
child: ProductCard(product: products[i]),
),
)
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) 大規模ウィジェットリビルドの回避
| 問題 | 解決策 |
|---|---|
| 大規模なbuildメソッドが全体をリビルド | 複数の小さなウィジェットに分割 |
| setStateがサブツリー全体をリビルド | 影響を受ける部分のみリビルド |
| 親ウィジェットの変更がサブツリーに影響 | const子コンポーネントで伝播をブロック |
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:大規模ウィジェットの分割
DART
import 'package:flutter/material.dart';
// BAD: entire page rebuilds on counter change
class BadPage extends StatefulWidget {
@override
State<BadPage> createState() => _BadPageState();
}
class _BadPageState extends State<BadPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
const HeavyHeader(), // Rebuilds unnecessarily!
const ProductGrid(), // Rebuilds unnecessarily!
Text('Count: $_count'), // Only this needs rebuild
ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
]);
}
}
// GOOD: only counter part rebuilds
class GoodPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(children: [
const HeavyHeader(), // Never rebuilds (const)
const ProductGrid(), // Never rebuilds (const)
const _CounterSection(), // Only this rebuilds
]);
}
}
// HeavyHeader / ProductGrid are placeholder example Widgets
class HeavyHeader extends StatelessWidget {
const HeavyHeader({super.key});
@override Widget build(BuildContext context) => const Text('Header');
}
class ProductGrid extends StatelessWidget {
const ProductGrid({super.key});
@override Widget build(BuildContext context) => const Text('Grid');
}
class _CounterSection extends StatefulWidget {
const _CounterSection();
@override
State<_CounterSection> createState() => _CounterSectionState();
}
class _CounterSectionState extends State<_CounterSection> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
Text('Count: $_count'),
ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
]);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
6. メモリ最適化
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:AutomaticKeepAliveClientMixin
DART
import 'package:flutter/material.dart';
// Product / ProductTile come from the ShopApp project, full definitions in Lessons 26/27
// Keep tab content alive when switching tabs
class ProductListTab extends StatefulWidget {
@override
State<ProductListTab> createState() => _ProductListTabState();
}
class _ProductListTabState extends State<ProductListTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; // Keep state when tab is hidden
@override
Widget build(BuildContext context) {
super.build(context); // Required for mixin
return ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductTile(product: products[i]),
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Disposeリソース解放
DART
import 'dart:async';
import 'package:flutter/material.dart';
// DetailPage / productStream come from the ShopApp project
class _DetailPageState extends State<DetailPage> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late TextEditingController _textController;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
_textController = TextEditingController();
_subscription = productStream.listen((_) => setState(() {}));
}
@override
void dispose() {
_controller.dispose(); // Release animation
_textController.dispose(); // Release text controller
_subscription?.cancel(); // Cancel stream
super.dispose();
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
7. ネットワーク最適化
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:cached_network_imageによる画像キャッシュ
DART
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependency: cached_network_image: ^3.0.0 (add to dependencies)
// product.imageUrl comes from ShopApp Product entity
CachedNetworkImage(
imageUrl: product.imageUrl,
placeholder: (context, url) => Container(
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
fadeInDuration: const Duration(milliseconds: 300),
memCacheWidth: 400, // Resize in memory to save RAM
maxWidth: 800, // Disk cache max width
)
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:ページネーションAPI + プリロード
DART
import 'package:flutter/material.dart';
// Dependency: no extra dependencies (Dart/Flutter standard library sufficient)
// Product / ProductTile / LoadingIndicator come from the ShopApp project
// api.fetchProducts comes from the ShopApp network layer
class PaginatedProductList extends StatefulWidget {
@override
State<PaginatedProductList> createState() => _PaginatedProductListState();
}
class _PaginatedProductListState extends State<PaginatedProductList> {
final _controller = ScrollController();
List<Product> _products = [];
int _page = 1;
bool _hasMore = true;
@override
void initState() {
super.initState();
_loadPage();
_controller.addListener(() {
// Preload when 2 screens away from bottom
if (_controller.position.pixels >= _controller.position.maxScrollExtent * 0.8) {
_loadPage();
}
});
}
Future<void> _loadPage() async {
if (!_hasMore) return;
final newProducts = await api.fetchProducts(page: _page, limit: 20);
setState(() {
_products.addAll(newProducts);
_page++;
_hasMore = newProducts.length >= 20;
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _controller,
itemCount: _products.length + (_hasMore ? 1 : 0),
itemExtent: 72, // Fixed height for performance
itemBuilder: (_, i) {
if (i == _products.length) return const LoadingIndicator();
return ProductTile(product: _products[i]);
},
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
8. 完全な例:ShopApp高性能商品リスト
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependencies: flutter_riverpod: ^2.0.0, cached_network_image: ^3.0.0
// Product / productProvider / cartProvider come from the ShopApp project, full definitions in Lessons 26/27
class OptimizedProductGrid extends ConsumerWidget {
const OptimizedProductGrid({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productProvider);
return productsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => ErrorWidget(e),
data: (products) => GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.7,
),
itemCount: products.length,
itemBuilder: (_, i) => RepaintBoundary(
key: ValueKey(products[i].id),
child: _OptimizedProductCard(product: products[i]),
),
),
);
}
}
class _OptimizedProductCard extends ConsumerWidget {
final Product product;
const _OptimizedProductCard({required this.product});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: RepaintBoundary( // Isolate image repaint
child: CachedNetworkImage(
imageUrl: product.imageUrl,
fit: BoxFit.cover,
memCacheWidth: 200, // Save memory
placeholder: (_, __) => Container(color: Colors.grey[200]),
errorWidget: (_, __, ___) => const Icon(Icons.image_not_supported),
),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.name, maxLines: 1, overflow: TextOverflow.ellipsis),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('\$${product.price.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.green)),
IconButton(
icon: const Icon(Icons.add_shopping_cart, size: 18),
onPressed: () => ref.read(cartProvider.notifier).addItem(product),
),
],
),
],
),
),
),
],
),
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q パフォーマンス分析にはdebugとprofileどちらを使うべきですか?
A profileモードを使う必要があります。debugモードには多数のチェックアサーションが含まれ、FPS計測が不正確になります。
Q 可能な限りconstを追加すべきですか?
A はい。constコンストラクタはゼロコスト最適化 — すべてのコンストラクタパラメータがコンパイル時定数の場合はいつでも追加できます。
Q RepaintBoundaryを多く追加しすぎるとデメリットはありますか?
A はい。各RepaintBoundaryは独立したペイントレイヤーを作成し、メモリ使用量が増加します。再描画が頻繁な分離ポイントにのみ追加してください。
Q なぜitemExtentがパフォーマンスを向上させるのですか?
A 固定アイテム高がある場合、ListViewは各子を測定する必要がなく — オフセットを直接計算でき、スクロールがスムーズになります。
Q CachedNetworkImageとImage.networkの違いは何ですか?
A Image.networkにはキャッシュがなく、毎回再ダウンロードします。CachedNetworkImageはディスク + メモリキャッシュを自動提供し、プレースホルダーとエラーウィジェットもサポートします。
Q メモリリークはどう検出しますか?
A DevToolsのMemoryパネルでメモリ推移を確認します。メモリが増え続けて戻らない場合、おそらくリークがあります。一般的な原因:Controllerの未破棄、Streamの未キャンセル。
📖 まとめ
- DevToolsがパフォーマンスボトルネックを特定:FPS / Rebuild / CPU / Memoryの4次元分析
- constコンストラクタはゼロコスト最適化 — 可能な限り追加
- RepaintBoundaryが再描画を分離、小さな変更による大範囲の再描画を防止
- 大規模ウィジェットを分割し、setStateが最小サブツリーにのみ影響するように
- CachedNetworkImageが画像をキャッシュ、AutomaticKeepAliveがTab状態を保持
📝 練習問題
- 基本 (⭐):DevToolsのWidget Rebuild Trackerを使って頻繁にリビルドされるウィジェットを見つけ、constで最適化してください。
- 中級 (⭐⭐):商品リストにRepaintBoundary + itemExtentを追加し、最適化前後のFPSを比較してください。
- チャレンジ (⭐⭐⭐):完全な高性能商品リストを実装してください:constウィジェット + RepaintBoundary + CachedNetworkImage(memCacheWidth) + ページネーションプリロード + AutomaticKeepAliveClientMixin、profileモードで10000アイテムの安定60fpsを達成。