# 2026年のFlutterパフォーマンス最適化:Impeller、リビルド、ベストプラクティス > 2026年にFlutterアプリを60または120fpsで安定させる方法。Impeller、規律あるウィジェットのリビルド、RepaintBoundary、DevToolsでのプロファイリングを解説します。 - Published: 2026-07-04 - Updated: 2026-07-07 - Author: SharpSkill - Tags: flutter, performance, impeller, best-practices, dart, mobile - Reading time: 9 min --- 2026年のFlutterのパフォーマンス最適化は、滑らかなアプリの作り方を変えた2つの転換に支えられています。レンダリングエンジンのImpellerがiOSとAndroidで既定として出荷されるようになったこと、そしてフレームワークのリビルドモデルが、ウィジェットツリーを小さく不変に保つ開発者に報いることです。この2つのてこを活用するチームは、フレームごとに手作業で調整することなく、60または120fpsを安定して維持します。 > **最も効果の大きい変更** > > ほとんどのFlutterアプリで最大の効果を生むのは、`const`コンストラクタを追加し、大きな`build`メソッドを分割することです。そうすれば`setState`の呼び出しが画面全体ではなくボタン1つだけを再描画します。 ## なぜImpellerがFlutterのパフォーマンス基準を引き直すのか ImpellerはFlutterのレンダリングエンジンであり、モバイルプラットフォーム全体でSkiaに代わって既定になりました。差が最も表れるのはアニメーションの最初の数秒です。Skiaはシェーダーをジャストインタイムで、ある効果が画面に初めて現れたときにコンパイルしていました。これがFlutterアプリが批判された、あの悪名高い初回実行時のカクつきを生んでいました。Impellerはそれらのシェーダーをビルド時に事前コンパイルするため、アニメーションの最初のフレームは100番目のフレームと同じくらい滑らかです。 Impellerは現代的なグラフィックスAPIも直接ターゲットにします。iOSではMetal、AndroidではVulkanを用い、古いAndroidハードウェア向けにはOpenGLのフォールバックがあります。[GitHub上のImpellerエンジンのドキュメント](https://github.com/flutter/flutter/blob/master/docs/engine/impeller/README.md)は、レンダリングアーキテクチャとプラットフォームサポートのマトリクスを詳しく説明しています。 | 観点 | Skia(旧来) | Impeller(2026の既定) | |--------|---------------|-------------------------| | シェーダーのコンパイル | 実行時、初回実行時のカクつきを引き起こす | 事前、ビルド時 | | iOSバックエンド | OpenGL / Metal | Metal | | Androidバックエンド | OpenGL | Vulkan、OpenGLフォールバック | | 最初のアニメーションの滑らかさ | ときどきカクつく | 一貫している | ほとんどのプロジェクトで移行は無料です。最近のFlutterリリースはImpellerを自動的に有効にし、古いシェーダーウォームアップの回避策(`--purge-persistent-cache`、同梱の`.sksl`ファイル)はもはや不要で、削除できます。FlutterがサポートするGLSLのサブセットに対して書かれたカスタムの`FragmentProgram`シェーダーも引き継がれます。Impellerがそれらを、フレームワーク自身の効果と同じオフラインパイプラインでコンパイルするためです。 アプリがどのエンジンで動作しているかの確認は1行で済みます。デバッグビルドは起動時にアクティブなバックエンドを出力し、DevToolsのインスペクタはアプリのレンダリング詳細でそれを報告します。Impellerが確認できたら、残りのパフォーマンス作業はウィジェット層へと上がります。そこではフレームワークが、画面が実際に変化するよりもはるかに頻繁にリビルドします。本ガイドの残りが焦点を当てるのはそこです。速いエンジンでも、1秒間に何百回も自らを再描画するウィジェットツリーは救えないからです。 ## constとウィジェット分割でFlutterのリビルドを減らす `setState`は毎回そのウィジェットをダーティとしてマークし、その要素とサブツリーについて`build`を再実行します。コツはそのサブツリーをできるだけ狭く保つことです。作業の大半は2つの習慣が担います。静的なウィジェットを`const`としてマークすること、そして状態を下へ押し下げて、状態を持つ部分を小さく保つことです。 `const`ウィジェットは単一のインスタンスに正規化されるため、親がリビルドされるとき、Flutterは同一の参照を比較し、変更がないと判断して、その枝の再描画を完全にスキップします。 ```dart // product_screen.dart // A cart update repaints the badge, not the header. class ProductScreen extends StatefulWidget { const ProductScreen({super.key}); @override State createState() => _ProductScreenState(); } class _ProductScreenState extends State { int _cartCount = 0; @override Widget build(BuildContext context) { return Column( children: [ const ProductHeader(), // const: never rebuilds on setState CartBadge(count: _cartCount), // only this subtree rebuilds FilledButton( onPressed: () => setState(() => _cartCount++), child: const Text('Add to cart'), ), ], ); } } ``` `analysis_options.yaml`で`prefer_const_constructors`のlintを有効にすると、この規律がコンパイラのチェックに変わり、`const`の機会が黙って後退することがなくなります。関連する`prefer_const_literals_to_create_immutables`ルールは、同じ保証をウィジェットに渡されるリストやマップにも広げます。 ウィジェットのキーもここで触れる価値があります。リストが並べ替えられたり、ウィジェットの型は同じでも同一性が変わったりするとき、`ValueKey`や`ObjectKey`があれば、Flutterは状態を破棄して作り直す代わりに、古い要素を正しい新しいウィジェットインスタンスに対応付けられます。キーの欠落は、動的リストでスクロール位置が失われたりアニメーションがリセットされたりする一般的な原因であり、それが引き起こす余分なリビルド作業は見落としやすいものです。 ## 変化したデータにリビルドを絞り込む 状態がツリーの高い位置にあると、うまく分割したレイアウトでも多くリビルドされます。対策は、値をブロードキャストし、単一のウィジェットにそれを聴かせることです。Flutterはまさにこのために`ValueNotifier`と`ValueListenableBuilder`を用意しており、外部パッケージは不要です。 ```dart // article_list.dart // A scroll-to-top button that rebuilds alone, not the whole page. class ArticleList extends StatefulWidget { const ArticleList({super.key}); @override State createState() => _ArticleListState(); } class _ArticleListState extends State { final _showFab = ValueNotifier(false); final _controller = ScrollController(); @override void initState() { super.initState(); // Update the notifier, not setState: the list never rebuilds _controller.addListener(() => _showFab.value = _controller.offset > 400); } @override void dispose() { _showFab.dispose(); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: ListView.builder( controller: _controller, itemCount: 200, itemBuilder: (_, i) => ListTile(title: Text('Item $i')), ), floatingActionButton: ValueListenableBuilder( valueListenable: _showFab, builder: (_, visible, __) => visible ? FloatingActionButton( onPressed: () => _controller.jumpTo(0), child: const Icon(Icons.arrow_upward), ) : const SizedBox.shrink(), ), ); } } ``` 状態管理ライブラリは同じ考え方を一般化します。Riverpodの`ref.watch(provider.select(...))`とProviderの`Selector`ウィジェットは、いずれも状態の選ばれた一部が変化したときだけリビルドします。これがデータの多い画面で滑らかかカクつくかを分ける決め手です。これらのライブラリ間のトレードオフは、[2026年のFlutter状態管理](/blog/flutter/flutter-state-management-2026-riverpod-bloc-getx)のガイドで扱っています。 > **リビルドの範囲に注意** > > 画面のルート近くで`setState`を呼んだり、ページ全体を最上位の`Consumer`で包んだりすると、変更のたびに何百ものウィジェットが静かに再描画されます。ある画面が軽いと決めつける前に、DevToolsのリビルドカウンタでプロファイルしてください。 ## RepaintBoundaryで高コストな再描画を分離する リビルドと再描画は別の段階です。ウィジェットはリビルドをスキップしても、騒がしい隣人がレイヤーを共有しているために再描画されることがあります。`RepaintBoundary`はサブツリーに独自のレイヤーを与えるため、アニメーションするグラフが静的なカードを毎フレーム再描画させることはできません。 ```dart // dashboard.dart // The live chart repaints on its own layer, cards stay untouched. class Dashboard extends StatelessWidget { const Dashboard({required this.ticker, super.key}); final Stream ticker; @override Widget build(BuildContext context) { return Column( children: [ // SummaryCards is static, so isolate it from the animation below const RepaintBoundary(child: SummaryCards()), RepaintBoundary( child: StreamBuilder( stream: ticker, builder: (_, snap) => LivePriceChart(value: snap.data ?? 0), ), ), ], ); } } ``` RepaintBoundaryは無料ではありません。1つごとにレイヤーを確保するため、あちこちにばらまくと利益より害が大きくなります。本当にアニメーションする領域や長いスクロールリストの周りで用い、タイムラインで効果を確認してください。[RepaintBoundaryのAPIリファレンス](https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html)は、デバッグの再描画オーバーレイが、実際に再描画されるレイヤーをどのように強調表示するかを説明しています。 ## buildメソッドを軽く、副作用なく保つ `build`メソッドはリビルドのたびに実行されるため、その中の高コストなものはすべて何倍にもなります。ルールは、`build`は状態を読み取ってウィジェットを返すだけ、それ以上はしないことです。`build`の中で`ScrollController`、`AnimationController`、`Future`を確保すると、毎フレーム作り直され、以前のインスタンスがリークします。それらのオブジェクトは`initState`か、`dispose`で解放される`late final`フィールドに属します。 重い同期処理は2つ目の罠です。`build`の中でJSONをパースしたり、大きなリストをフィルタしたり、日付をフォーマットしたりすると、たとえ元のデータが変わっていなくても、その処理が再描画のたびに再実行されます。入力が変わったときに結果を一度だけ計算してキャッシュするか、フレームワークが1フレームをブロックして待たずに済むよう`FutureBuilder`に移してください。他の値から導かれる値については、依存関係が変わったときだけ再計算するゲッターの背後でメモ化します。 `Opacity`ウィジェットは名指しする価値のある具体的な問題児です。サブツリーを`Opacity`で包むと画面外の`saveLayer`が強制されます。これはパイプラインで最も高コストな操作の1つです。フェードアニメーションには`AnimatedOpacity`や`FadeTransition`のほうが安く、固定の色合いには`foregroundDecoration`やシェーダーを通してペイントレベルで色を適用すれば、レイヤーを完全に避けられます。同じ注意は、大きな面でアンチエイリアスを効かせた`ClipPath`や`ClipRRect`にも当てはまり、これらも裏で`saveLayer`を発生させます。 最後に、`const`のコレクションと、ビルドごとに新しいクロージャを捕捉しないコールバックを優先してください。既定として渡す`const []`と、インラインのラムダの代わりのメソッド参照は、どちらもウィジェットの等価チェックを安く保ち、前述の`const`によるショートサーキットを実際に発火させます。 ## 長いリストと画像を効率的に構築する 長いフィードや画像グリッドは、素朴なFlutterコードがフレームを落とす場所です。2つのルールがほとんどのケースをカバーします。行を遅延的に構築すること、そして画像をソース解像度ではなく表示されるサイズでデコードすることです。 ```dart // feed.dart // Lazy rows plus right-sized image decoding keep scrolling at 60fps. class Feed extends StatelessWidget { const Feed({required this.posts, super.key}); final List posts; @override Widget build(BuildContext context) { return ListView.builder( itemCount: posts.length, // build on demand, not all at once cacheExtent: 600, // pre-build past the viewport edge itemBuilder: (context, index) { final post = posts[index]; return ListTile( leading: Image.network( post.thumbnailUrl, width: 48, height: 48, cacheWidth: 96, // decode at display size, save memory ), title: Text(post.title), ); }, ); } } ``` 3000ピクセルのフル解像度画像を48ピクセルのアバターに押し込むと、その1つ1つでメモリとデコード時間が無駄になります。`cacheWidth`を設定するとエンジンにターゲットサイズでデコードするよう指示でき、これだけでリストのメモリ使用量を1桁分削減できることがあります。Flutterの[パフォーマンスのベストプラクティス](https://docs.flutter.dev/perf/best-practices)は、同じ原則に従う`Opacity`、`saveLayer`、クリップの使い方に関するさらなる指針をまとめています。 ## 2026年のFlutter最適化をDevToolsで測定する 上記のすべてのルールは、想定するのではなく検証すべきです。Flutter DevToolsが信頼できる情報源です。パフォーマンスオーバーレイ(デバッグコンソールの`P`、または`flutter run --profile`)はUIとラスターの2つのグラフを描き、緑の線を越えるバーはすべて落ちたフレームです。タイムラインビューは次に、各遅いフレームを特定の`build`、レイアウト、ペイントの呼び出しに帰属させます。 必ず実機で`--profile`モードでプロファイルしてください。デバッグビルドは最適化されていないDartを実行し、あらゆる測定値を膨らませるため、デバッグでカクつく画面がコンパイル後には完璧に動くことがよくあります。[DevToolsのパフォーマンスガイド](https://docs.flutter.dev/tools/devtools/performance)はフレームチャートの読み方とジャンクの原因の見つけ方を順を追って説明しており、同じプロファイリングの勘所は[Flutterのアニメーションとレンダリング](/technologies/flutter/interview-questions/animations)に関する面接でも登場します。 > **素早いプロファイリングのループ** > > プロファイルモードで実行し、カクつく操作を再現し、タイムラインを開いて、フレームを継続時間で並べ替えます。最悪のフレームはほぼ必ず、単一の過大なリビルドかキャッシュされていない画像を指し示します。これは推測するよりはるかに速い方法です。 より広い[Flutterテクノロジーハブ](/technologies/flutter)は、本番のパフォーマンス戦略を完成させる状態管理、テスト、レンダリングの各トピックを結びつけます。 ## まとめ 2026年のFlutterのパフォーマンスは、細かなトリックよりも、一貫して適用する短い習慣のリストに関わります。 - Impeller上で出荷し、旧来のシェーダーウォームアップコードを削除する。最初のフレームのジャンク問題はエンジンレベルで解決済みです。 - 静的なウィジェットを`const`としてマークし、`prefer_const_constructors`のlintで強制する。 - 状態を持つウィジェットを小さく分割し、各`setState`が可能な限り狭いサブツリーを再描画するようにする。 - 変化する値を、画面全体をリビルドする代わりに`ValueListenableBuilder`や`select`スタイルのAPIでブロードキャストする。 - 本当にアニメーションする領域を`RepaintBoundary`で包む。ただし増やす前に測定する。 - リストは`ListView.builder`で構築し、画像は`cacheWidth`を使って表示サイズでデコードする。 - 実機で`--profile`モードでプロファイルし、次に何を最適化するかを直感ではなくDevToolsに決めさせる。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/flutter/flutter-performance-optimization-2026-impeller-rebuilds