対象者
- Flutterを使用してUIのパフォーマンスを最適化したい開発者
- アニメーションやスクロールのスムーズさを向上させたいエンジニア
- RepaintBoundaryの効果的な使用方法を理解し、適切に適用したい方
はじめに
FlutterでUIを構築する際、パフォーマンスの最適化は重要な要素です。特にアニメーションや頻繁に更新されるウィジェットを含む複雑なUIでは、不要な再描画によるパフォーマンスの低下が顕著になります。
Flutterの描画システムでは、ウィジェットツリーに変更があると、そのウィジェットと子孫ウィジェットが再描画されます。しかし、一部のウィジェットの変更だけでもツリー全体が再描画されると、無駄な処理が発生し、パフォーマンスに影響を与えます。
この問題を解決するためにFlutterには RepaintBoundary
というウィジェットが用意されています。本記事では RepaintBoundary
の仕組みと活用方法について解説します。
RepaintBoundaryとは
RepaintBoundary
は、その子ウィジェットとそのサブツリーを、親ウィジェットの更新による不要な再描画から分離するウィジェットです。
仕組み
RepaintBoundary
で囲まれたウィジェットは 独立したレイヤーとして扱われる。- 内部のウィジェットが更新されても 外部には影響しない。
- 外部が更新されても 内部のウィジェットは再描画されない。
- 再描画範囲を限定することでパフォーマンス向上 が期待できる。
RepaintBoundaryのユースケース
RepaintBoundary
は以下のような場合に特に効果的です。
1. 複雑なアニメーションの最適化
アニメーションを含むウィジェットを RepaintBoundary
で囲むことで、アニメーションによる再描画を分離し、他のウィジェットへの影響を最小限にできます。
2. スクロールパフォーマンスの向上
ListView
や GridView
などのスクロール可能なリストでは、スクロール中に多くのウィジェットが再描画されます。RepaintBoundary
を使用することで、リスト全体の不要な再描画を防ぐことができます。
3. インタラクティブなウィジェットの最適化
スライダーやボタン、チェックボックスなどの頻繁に更新されるウィジェットは RepaintBoundary
で囲むことで、周囲のUIへの影響を減らせます。
4. 頻繁に更新されるウィジェット
時計やプログレスバーのような頻繁に更新されるウィジェットに RepaintBoundary
を適用すると、再描画の負荷を軽減できます。
RepaintBoundaryの注意点
1. 過剰な使用は避ける
RepaintBoundary
は、新しいレイヤーを作成するため、メモリ使用量が増加します。必要以上に使用すると、逆にパフォーマンスが低下する可能性があります。
2. パフォーマンスオーバーレイで確認
Flutterの Performance Overlay
を有効にすることで、RepaintBoundary
が適切に機能しているかどうかを可視化できます。
3. 適切な場所に配置
頻繁に更新されるウィジェットやアニメーションを含むウィジェットに RepaintBoundary
を配置すると、再描画の負荷を最適化できます。
4. デバッグフラグ debugRepaintRainbowEnabled
を活用
再描画の範囲を可視化するには、debugRepaintRainbowEnabled
を true
に設定します。
void main() {
debugRepaintRainbowEnabled = true;
runApp(MyApp());
}
ベストプラクティス
RepaintBoundaryの使用は控えめに
RepaintBoundary
自体が若干のオーバーヘッドを追加するため、多用すると逆効果になる可能性があります。- 常にパフォーマンスオーバーレイと再描画レインボーを使用して、不要な再描画が発生している箇所を特定しましょう。
静的または独立したセクションをラップ
- 頻繁に再描画する必要のないUIの部分を
RepaintBoundary
でラップすると効果的です。
RepaintBoundaryのコストに注意
RepaintBoundary
はパフォーマンスを向上させる一方で、新たなレイヤーを作成するため、CPUとメモリのオーバーヘッドが発生します。- 複雑なアニメーションや頻繁に変更されるウィジェットの分離 など、明確なメリットがある場所に限定して使用しましょう。
- 過剰に使用すると、境界が蓄積され、パフォーマンスの利点が相殺される 可能性があります。
Performance Overlay
などのツールを活用し、影響を監視・テストすることを推奨します。
RepaintBoundaryの実装例
このコードでは、RepaintBoundary
を使用することで アニメーションのパフォーマンスをどのように最適化できるか を確認できます。
具体的には、以下の点を検証できます:
RepaintBoundary
なし の場合、アニメーションの影響が親ウィジェット (Scaffold
) に及び、不要な再描画が発生すること。RepaintBoundary
あり の場合、アニメーションが個別のレイヤーで処理され、親ウィジェットの不要な再描画を防ぐことができること。debugRepaintRainbowEnabled
を利用し、どの部分が再描画されているのかを視覚的に確認 できること。
この実験を通じて、Flutter における RepaintBoundary
の実際の効果と適用すべき場面を理解できる ようになります。
RepaintBoundary
の適用箇所
このコードでは RollingDash
(回転するアイコン) に RepaintBoundary
を適用しています。
final isAvailable = _counter % 2 == 1;
return isAvailable
? RepaintBoundary(
child: RollingDash(
angele: angle,
),
)
: RollingDash(angele: angle);
_counter
の値によってRepaintBoundary
を適用するかどうかを切り替えるisAvailable == true
のときはRepaintBoundary
でRollingDash
を囲むisAvailable == false
のときはRollingDash
をそのまま描画
目的:
RepaintBoundary
を適用した場合としなかった場合の違いを確認できるようにするRepaintBoundary
を適用することで、親 (Scaffold
) の再描画を防ぐことができる
RollingDash
のアニメーション**
class RollingDash extends StatelessWidget {
const RollingDash({
super.key,
required this.angele,
});
final double angele;
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: angele,
child: Icon(
Icons.flutter_dash,
size: 128,
),
);
}
}
_controller.value
に基づいてIcons.flutter_dash
を回転Transform.rotate(angle: angele)
を使用し、angele
の値に応じて回転角度を変更
ポイント:
RollingDash
はAnimatedBuilder
のbuilder
内で回転する- この回転が原因で再描画が発生する
- そのため、
RepaintBoundary
でラップすることで、影響範囲を限定できる
debugRepaintRainbowEnabled
を活用
このコードでは、Flutter の 再描画デバッグ機能 を利用しています。
void main() {
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}
- 再描画されるウィジェットを可視化 できる
RepaintBoundary
の効果を検証可能RollingDash
のアニメーション中に、 どの部分が再描画されているかが色付きで表示される
デバッグ方法
debugRepaintRainbowEnabled = true;
を有効化RepaintBoundary
のchild
にRollingDash
を設定isAvailable
を切り替えて、 再描画範囲の違い を確認
RepaintBoundary
の効果を検証**
RepaintBoundary
を適用しない場合
RollingDash
のアニメーションにより、 親ウィジェット (Scaffold
) まで再描画される- 不要な再描画が発生し、フレームレートが低下する可能性あり
RepaintBoundary
を適用した場合
RollingDash
のアニメーションが 個別のレイヤーで処理される- 親 (
Scaffold
) や他の UI 部分には影響を与えない - パフォーマンスが向上し、スムーズなアニメーションを実現
まとめ
RepaintBoundary
は、Flutterアプリのパフォーマンス最適化に役立つ強力なツールです。ただし、過剰な使用はメモリ消費を増やすため、適切な場所に適用することが重要です。パフォーマンスオーバーレイを使用して最適な適用箇所を特定し、よりスムーズなUIを実現しましょう!
参考
- youtube.com – RepaintBoundary (Widget of the Week)
- api.flutter.dev – RepaintBoundary class – widgets library – Dart API
- 10 Flutter Widgets Probably Haven’t Heard Of (But Should Be Using!)
- The Magic of Repaint Boundary!
ソース(main.dartにコピペして動作確認用)
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
void main() {
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
showPerformanceOverlay: true,
checkerboardRasterCacheImages: true,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
late final _controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 1000,
))
..repeat();
@override
Widget build(BuildContext context) {
final isAvailable = _counter % 2 == 1;
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final angle = _controller.value * 2 * math.pi;
return Center(
child: isAvailable
? RepaintBoundary(
child: RollingDash(
angele: angle,
))
: RollingDash(angele: angle),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: const Icon(Icons.add),
),
);
}
}
class RollingDash extends StatelessWidget {
const RollingDash({
super.key,
required this.angele,
});
final double angele;
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: angele,
child: Icon(
Icons.flutter_dash,
size: 128,
),
);
}
}
-
Next
Flutter開発 with Devin