Flutter: パフォーマンス最適化

パフォーマンスはユーザー体験の基盤 — 60fpsは約束、59fpsは失敗です。

📋 前提条件: 以下を先にマスターしている必要があります

1. このレッスンで学ぶこと


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. パフォーマンス最適化概要

100%
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の未キャンセル。

📖 まとめ


📝 練習問題

  1. 基本 (⭐):DevToolsのWidget Rebuild Trackerを使って頻繁にリビルドされるウィジェットを見つけ、constで最適化してください。
  2. 中級 (⭐⭐):商品リストにRepaintBoundary + itemExtentを追加し、最適化前後のFPSを比較してください。
  3. チャレンジ (⭐⭐⭐):完全な高性能商品リストを実装してください:constウィジェット + RepaintBoundary + CachedNetworkImage(memCacheWidth) + ページネーションプリロード + AutomaticKeepAliveClientMixin、profileモードで10000アイテムの安定60fpsを達成。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%