はじめに
モバイルアプリ開発において、クロスプラットフォーム開発の選択は重要な判断です。Flutterは単一のコードベースでiOSとAndroidの両プラットフォームに対応できる利点がありますが、パフォーマンス面での懸念が常に付きまといます。
本記事では、iOSとAndroidの両プラットフォームにおいて、Flutterとネイティブ実装のパフォーマンスを具体的な数値とともに比較検証しました。
検証内容
- リスト表示
- 画像とテキストで構成されたアイテム(縦:100個)
- Lottieアニメーション表示
- ネット上にあるjsonファイルを再生(5列×20行)
- 各実装でのCPU使用率、メモリ使用量、FPSを計測
計測
-
デバイス:iPhone15(iOS)、Pixel8 (Android)
-
Flutter vs iOS
- CPU 使用率、メモリ使用量
- xcode の Debug navigator を使用
- 5 回計測して平均値を算出
- FPS
- xcode の Instruments の TimeProfiler で計測
- スクロール操作をしながら計測する
- CPU 使用率、メモリ使用量
-
Flutter vs Android
- CPU 使用率、メモリ使用量
- AndroidStudio の Profier の SystemTrace と HeapDump で計測
- 5 回計測して平均値を算出
- FPS
- Flutter DevTools と Profier の CaptureSystemActivities で計測
- スクロール操作をしながら計測する
- CPU 使用率、メモリ使用量
実装方法
iOS:ListView実装
struct ListView: View {
var body: some View {
let url = URL(string: "image_url")
List {
ForEach(0..<100) { _ in
HStack {
AsyncImage(url: url){
image in image.image?.resizable()
}.frame(width: 160, height: 100)
VStack{
Text("テストタイトル").frame(maxWidth: .infinity, alignment: .leading)
Text("テキストテキストテキスト").frame(maxWidth: .infinity, alignment: .leading)
}
}
}.listRowSeparator(.hidden)
}.listStyle(.plain)
}
}
iOS:Animation実装
struct AnimationView: View {
var body: some View {
let url = URL(string: "animation_url")
List {
ForEach(0..<20) { _ in
HStack {
ForEach(0..<5) {
_ in
if let url = url {
LottieView(path: url)
.background(.cyan)
}
}
}.frame(width: 400, height: 70)
}.listRowSeparator(.hidden)
}.listStyle(.plain)
}
}
Android ListView実装
@Composable
fun VerticalListView() {
LazyColumn {
items(100) {
ListItem()
}
}
}
@Composable
fun ListItem() {
Row(modifier = Modifier.padding(4.dp)) {
Image(
painter = rememberImagePainter("image_url"),
contentDescription = null,
modifier = Modifier
.width(160.dp)
.height(100.dp),
contentScale = ContentScale.Crop
)
Column(modifier = Modifier.padding(start = 8.dp)) {
Text(text = "テストタイトル")
Text(text = "テキストテキストテキスト")
}
}
}
Android Animation実装
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AnimationGrid() {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Animation Grid") }
)
}
) { paddingValues ->
LazyVerticalGrid(
columns = GridCells.Fixed(5),
contentPadding = paddingValues,
modifier = Modifier.fillMaxSize()
) {
items(100) { _ ->
LottieAnimationItem()
}
}
}
}
@Composable
fun LottieAnimationItem() {
val compositionResult = rememberLottieComposition(
LottieCompositionSpec.Url("animation_url")
)
val composition = compositionResult.value
val isVisible = LocalWindowInfo.current.isWindowFocused
val progressState = animateLottieCompositionAsState(
composition = composition,
iterations = LottieConstants.IterateForever,
isPlaying = isVisible // 画面に表示されている時だけアニメーション
)
val progress = progressState.value
LottieAnimation(
composition = composition,
progress = { progress },
modifier = Modifier
.aspectRatio(1f)
.graphicsLayer {
renderEffect = android.graphics.RenderEffect
.createBlurEffect(0f, 0f, android.graphics.Shader.TileMode.CLAMP)
.asComposeRenderEffect()
}
)
}
Flutter ListView実装
return MaterialApp(
title: 'Flutter ListView',
theme: ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Row(
children: <Widget>[
Container(
width: 160,
height: 100,
margin: const EdgeInsets.only(top: 4, bottom: 4),
child: Image.network("image_url")
),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("テストタイトル"),
Text("テキストテキストテキスト"),
],
)
],
);
},
),
));
Flutter Animation実装
return MaterialApp(
title: 'Animation Grid',
home: Scaffold(
appBar: AppBar(
title: Text('Animation Grid'),
),
body: GridView.count(
crossAxisCount: 5,
children: List.generate(100, (index) {
return Lottie.network("animation_url");
}),
),
),
);
計測結果(ListView)
1. CPU 使用率
iOS
| 実装 | 起動時 | スクロール時 | 通常時 |
|---|---|---|---|
| Native | 78% | 28.2-41% | 0-0.2% |
| Flutter | 95.6% | 31.4-36.8% | 0-0.8% |
Android
| 実装 | 起動時 | スクロール時 | 通常時 |
|---|---|---|---|
| Native | 14.0% | 2.9-10.0% | 0% |
| Flutter | 18.0% | 5.1-28.8% | 0% |
2. メモリ使用量
iOS
| 実装 | 起動時 | スクロール時 | 通常時 |
|---|---|---|---|
| Native | 17.22MB | 20.1-21.14MB | 17.48-20.8MB |
| Flutter | 97.5MB | 104.4-107.84MB | 90.62-100.02MB |
Android
| 実装 | 起動時 | スクロール時 | 通常時 |
|---|---|---|---|
| Native | 148.4MB | 159.4-184.4MB | 186.8-189.4MB |
| Flutter | 314.2MB | 379.4-403.4MB | 390.0-390.2MB |
3. FPS
iOS
| 実装 | FPS |
|---|---|
| Native | 49-60fps |
| Flutter | 59-60fps |
Android
| 実装 | FPS |
|---|---|
| Native | 60fps |
| Flutter | 60fps |
計測結果(Animation)
1. CPU 使用率
iOS
| 実装 | 起動時 | スクロール時 | 通常時 |
|---|---|---|---|
| Native | 105.8% | 37.8-65.6% | 0% |
| Flutter | 157.8% | 146-156.4% | 124-142% |
Android
| 実装 | 起動時 | スクロール時 | 通常時 |
|---|---|---|---|
| Native | 17.6% | 9.1-24.2% | 8.2-19.6% |
| Flutter | 26.9% | 12.1-40.8% | 8.0-37.5% |
2. メモリ使用量
iOS
| 実装 | 起動時 | スクロール時 | 通常時 |
|---|---|---|---|
| Native | 105.8% | 37.8-65.6% | 0% |
| Flutter | 157.8% | 146-156.4% | 124-142% |
Android
| 実装 | 起動時 | スクロール時 | 通常時 |
|---|---|---|---|
| Native | 225.2MB | 335.0-401.6MB | 239.6-282.8MB |
| Flutter | 220.2MB | 576.8-609.8MB | 417.2-426.6MB |
3. FPS
iOS
| 実装 | FPS |
|---|---|
| Native | 59-60fps |
| Flutter | 39-49fps |
Android
| 実装 | FPS |
|---|---|
| Native | 58fps |
| Flutter | 23-25fps |
結論
本検証により、パフォーマンス面ではネイティブ実装が全体的に優位であることが分かりました。
パフォーマンス比較結果
Android
- CPU 使用率、メモリ使用量、FPS においてネイティブが優位
- メモリ使用量は Flutter がネイティブの約 2 倍
- アニメーション処理で Flutter の FPS 低下が顕著 (Flutter: 23-25fps vs Native: 58fps)
- CPU 使用率は軽量アプリ(ListView)では差が小さいが、アニメーション処理で差が拡大
iOS
- CPU 使用率、メモリ使用量、FPS においてネイティブが優位
- メモリ使用量は Flutter がネイティブの約 5 倍
- アニメーション処理で Flutter の FPS 低下が顕著(Native: 59-60fps vs Flutter: 39-49fps)
- CPU 使用率はアニメーション処理で Flutter が大幅に高い(Flutter: 124-157% vs Native: 37-65%)
アプリケーション別推奨事項
ネイティブを推奨
- アニメーションを多用するアプリ(FPS 差が顕著)
- 低スペック端末でも快適に動作させたい場合
- パフォーマンスおよびリソース効率を重視する場合
- 長期的な保守・最適化を重視する場合
Flutter も選択可能
- 軽量な UI 中心のアプリ(ListView レベルでは FPS 差が小さい)
- 開発効率とのトレードオフを重視する場合(1 つのコードベースで両プラットフォーム対応)
参考情報: 起動時間、UI初期化時間について
※以下の起動時間、UI初期化時間の計測は、Flutter とネイティブでレンダリングシステムが根本的に異なるため、直接比較は参考程度に留めることを推奨します。
計測方法
iOS
- 合計起動時間
- xcode の profile の App Launched で計測
-
https://qiita.com/_asa08_/items/914c6bcf69ec3f6cc906
- Initializing から Initial Frame Rendering が完了するまで
- 30 回計測して平均値を算出
- UI 初期化(レンダリング時間)
- iOS
- Initial Frame Rendering (IFR)
- xcode の profile の App Launched で計測できる
- 30 回計測して平均値を算出
- Initial Frame Rendering (IFR)
- Flutter
- WidgetsBinding.instance.addPostFrameCallback
- ウィジェットツリーが描画された後に呼ばれる
- 5 回計測して平均値を算出
- WidgetsBinding.instance.addPostFrameCallback
- iOS
Android
- 合計起動時間(reportFullyDrawn()まで)
-
ComponentActivityのreportFullyDrawn()で計測 - https://developer.android.com/topic/performance/vitals/launch-time?hl=ja#retrieve-TTFD
- 30 回計測して平均値を算出
-
- UI 初期化(レンダリング時間)
- Android
-
SideEffectからdoOnPreDrawまでの時間を計測- 5 回計測して平均値を算出
-
- Flutter
- WidgetsBinding.instance.addPostFrameCallback
- ウィジェットツリーが描画された後に呼ばれる
- 5 回計測して平均値を算出
- WidgetsBinding.instance.addPostFrameCallback
- Android
計測の制限事項
- iOS: Skia エンジン vs ネイティブ UIKit/SwiftUI の描画システムの違い
- Android: Flutter Engine vs Android View System の初期化タイミングの違い
- レンダリングの違い: Flutter の Skia エンジンによる独自描画とネイティブの標準 UI フレームワークでは描画完了の定義が異なる
起動時間計測結果
ListView 実装
iOS
| 実装 | UI 初期化 | 合計起動時間 |
|---|---|---|
| Native | 34.35ms | 453ms |
| Flutter | 195ms | 458ms |
Android
| 実装 | UI 初期化 | 合計起動時間 |
|---|---|---|
| Native | 514.6ms | 166ms |
| Flutter | 144.4ms | 198ms |
Animation 実装
iOS
| 実装 | UI 初期化 | 合計起動時間 |
|---|---|---|
| Native | 43.29ms | 472ms |
| Flutter | 186.4ms | 454ms |
Android
| 実装 | UI 初期化 | 合計起動時間 |
|---|---|---|
| Native | 563.6ms | 174ms |
| Flutter | 157.8ms | 209ms |
「Android vs Flutter」の計測にて、onCreate()からonResume()を計測しましたが、Flutterには独自のUIライフサイクル(StatefulWidget)があり、onResume()時点でUIの初期化が完全に完了していない可能性があります。
参考までにFlutterとNativeにおいてLottieのローディング完了までを再計測してみました。
https://zenn.dev/mukkun69n/articles/552173cb084e18#statefulwidget%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB
| 実装 | Lottieローディング時間 |
|---|---|
| Native | 165ms |
| Flutter | 82.9ms |