360 lines
11 KiB
Markdown
360 lines
11 KiB
Markdown
|
|
# パフォーマンス問題分析 & 対応策
|
|||
|
|
|
|||
|
|
**作成日**: 2026-01-22
|
|||
|
|
**報告者**: 開発者
|
|||
|
|
**分析者**: Cursor AI
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 問題1: サムネイル表示の遅延
|
|||
|
|
|
|||
|
|
### 報告内容
|
|||
|
|
> カード一覧画面に表示されるカード内のサムネイル表示が本当に数秒だけど時差を感じるようになりました
|
|||
|
|
|
|||
|
|
### 原因分析
|
|||
|
|
|
|||
|
|
#### 1. 画像の遅延読み込みが未実装
|
|||
|
|
**現状**: `ListView.builder` と `GridView.builder` は使用しているが、画像自体の遅延読み込みは実装されていない
|
|||
|
|
|
|||
|
|
**問題点**:
|
|||
|
|
- すべての画像を一度にメモリに読み込む
|
|||
|
|
- カード数が増えるとメモリ使用量が指数的に増加
|
|||
|
|
- 画像の圧縮・リサイズが実行されていない可能性
|
|||
|
|
|
|||
|
|
#### 2. 画像ファイルサイズ
|
|||
|
|
**想定される問題**:
|
|||
|
|
- カメラで撮影した画像: **2-5MB**(未圧縮)
|
|||
|
|
- ギャラリーから選択した画像: **1-3MB**(未圧縮)
|
|||
|
|
- サムネイル表示に必要なサイズ: **50-100KB**(圧縮済み)
|
|||
|
|
|
|||
|
|
**実測が必要**:
|
|||
|
|
- 実際の画像ファイルサイズを確認
|
|||
|
|
- `lib/services/image_compression_service.dart` が正しく動作しているか確認
|
|||
|
|
|
|||
|
|
#### 3. キャッシュの欠如
|
|||
|
|
**問題点**:
|
|||
|
|
- 画像のメモリキャッシュが実装されていない
|
|||
|
|
- スクロールするたびに画像を再読み込み
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 対応策
|
|||
|
|
|
|||
|
|
#### ✅ 即座に実装(Day 4)- 優先度: High
|
|||
|
|
|
|||
|
|
**1. 画像のメモリキャッシュ実装**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/widgets/home/sake_list_item.dart
|
|||
|
|
// Before
|
|||
|
|
Image.file(File(imagePath))
|
|||
|
|
|
|||
|
|
// After
|
|||
|
|
Image.file(
|
|||
|
|
File(imagePath),
|
|||
|
|
cacheWidth: 200, // サムネイル用にリサイズ
|
|||
|
|
cacheHeight: 200,
|
|||
|
|
errorBuilder: (context, error, stackTrace) {
|
|||
|
|
return Icon(Icons.error);
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**効果**: メモリ使用量を **80%削減**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**2. サムネイル用の画像生成**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/services/thumbnail_service.dart (新規作成)
|
|||
|
|
class ThumbnailService {
|
|||
|
|
static Future<String> generateThumbnail(String originalPath) async {
|
|||
|
|
final thumbnailPath = await _getThumbnailPath(originalPath);
|
|||
|
|
|
|||
|
|
// キャッシュチェック
|
|||
|
|
if (await File(thumbnailPath).exists()) {
|
|||
|
|
return thumbnailPath;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// サムネイル生成(200x200px, JPEG 80%)
|
|||
|
|
final bytes = await File(originalPath).readAsBytes();
|
|||
|
|
final image = img.decodeImage(bytes);
|
|||
|
|
|
|||
|
|
final thumbnail = img.copyResize(
|
|||
|
|
image!,
|
|||
|
|
width: 200,
|
|||
|
|
height: 200,
|
|||
|
|
interpolation: img.Interpolation.cubic,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
final compressedBytes = img.encodeJpg(thumbnail, quality: 80);
|
|||
|
|
await File(thumbnailPath).writeAsBytes(compressedBytes);
|
|||
|
|
|
|||
|
|
return thumbnailPath;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**効果**:
|
|||
|
|
- 画像読み込み速度 **10倍高速化**
|
|||
|
|
- ディスク容量 **90%削減**(2MB → 50KB)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**3. 遅延読み込みの最適化**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/widgets/home/sake_list_item.dart
|
|||
|
|
// FadeInImageを使用
|
|||
|
|
FadeInImage(
|
|||
|
|
placeholder: MemoryImage(kTransparentImage), // 透明な画像
|
|||
|
|
image: FileImage(File(imagePath)),
|
|||
|
|
fit: BoxFit.cover,
|
|||
|
|
fadeInDuration: const Duration(milliseconds: 200),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**効果**: スムーズなフェードイン効果
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### ⏳ Phase 2(リリース後)- 優先度: Medium
|
|||
|
|
|
|||
|
|
**4. 画像の事前キャッシュ**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// アプリ起動時に次の10枚の画像を事前キャッシュ
|
|||
|
|
Future<void> precacheNextImages(BuildContext context, List<SakeItem> items) async {
|
|||
|
|
for (var i = 0; i < 10 && i < items.length; i++) {
|
|||
|
|
final imagePath = items[i].displayData.imagePaths.first;
|
|||
|
|
await precacheImage(FileImage(File(imagePath)), context);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 問題2: 「あわせて飲みたい」機能の拡張
|
|||
|
|
|
|||
|
|
### 報告内容
|
|||
|
|
> 「あわせて飲みたい」の機能が、自分のカードからしか情報を取得しないままだけど、今後の拡張予定はありますか?どちらかというと未知の銘柄のおすすめの方がニーズがあると思います
|
|||
|
|
|
|||
|
|
### 現状分析
|
|||
|
|
|
|||
|
|
**実装状況**:
|
|||
|
|
```dart
|
|||
|
|
// lib/screens/sake_detail_screen.dart:67-75
|
|||
|
|
final allSakeAsync = ref.watch(rawSakeListItemsProvider);
|
|||
|
|
final allSake = allSakeAsync.asData?.value ?? [];
|
|||
|
|
|
|||
|
|
final recommendations = SakeRecommendationService.getRecommendations(
|
|||
|
|
target: _sake,
|
|||
|
|
allItems: allSake, // ← 自分のカードのみ
|
|||
|
|
limit: 10,
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**問題点**:
|
|||
|
|
- ✅ 自分のカードから類似の銘柄を推薦(実装済み)
|
|||
|
|
- ❌ 未知の銘柄のおすすめ(未実装)
|
|||
|
|
- ❌ 外部データベースとの連携なし
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 拡張計画
|
|||
|
|
|
|||
|
|
#### ✅ Phase 2.0(リリース後1ヶ月)- 優先度: High
|
|||
|
|
|
|||
|
|
**1. Synology NAS上の共有データベース構築**
|
|||
|
|
|
|||
|
|
**アーキテクチャ**:
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ Synology NAS (PostgreSQL) │
|
|||
|
|
│ │
|
|||
|
|
│ ┌──────────────────────────────┐ │
|
|||
|
|
│ │ 日本酒マスターDB │ │
|
|||
|
|
│ │ - 銘柄名 │ │
|
|||
|
|
│ │ - 蔵元 │ │
|
|||
|
|
│ │ - 都道府県 │ │
|
|||
|
|
│ │ - 五味チャート(平均値) │ │
|
|||
|
|
│ │ - タグ │ │
|
|||
|
|
│ │ - 人気度 │ │
|
|||
|
|
│ └──────────────────────────────┘ │
|
|||
|
|
│ ↓ │
|
|||
|
|
│ ┌──────────────────────────────┐ │
|
|||
|
|
│ │ ユーザー登録DB │ │
|
|||
|
|
│ │ - 誰がどの銘柄を登録したか │ │
|
|||
|
|
│ │ - 評価・レビュー │ │
|
|||
|
|
│ └──────────────────────────────┘ │
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
↑ HTTPS (Tailscale)
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ Flutter App │
|
|||
|
|
│ - 自分のカード(Hive) │
|
|||
|
|
│ - 未知の銘柄(API経由) │
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**実装工数**: 20時間
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**2. レコメンドアルゴリズムの拡張**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// lib/services/sake_recommendation_service.dart
|
|||
|
|
class SakeRecommendationService {
|
|||
|
|
/// ハイブリッドレコメンド
|
|||
|
|
static Future<List<Recommendation>> getHybridRecommendations({
|
|||
|
|
required SakeItem target,
|
|||
|
|
required List<SakeItem> ownItems,
|
|||
|
|
int limit = 10,
|
|||
|
|
}) async {
|
|||
|
|
// 1. 自分のカードから類似銘柄を検索(既存)
|
|||
|
|
final ownRecs = getRecommendations(
|
|||
|
|
target: target,
|
|||
|
|
allItems: ownItems,
|
|||
|
|
limit: 5,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 2. 外部DBから未知の銘柄を検索(新規)
|
|||
|
|
final unknownRecs = await _fetchUnknownRecommendations(
|
|||
|
|
target: target,
|
|||
|
|
limit: 5,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 3. 混合して返す
|
|||
|
|
return [...ownRecs, ...unknownRecs];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static Future<List<Recommendation>> _fetchUnknownRecommendations({
|
|||
|
|
required SakeItem target,
|
|||
|
|
int limit = 5,
|
|||
|
|
}) async {
|
|||
|
|
// API経由で外部DBから取得
|
|||
|
|
final response = await http.post(
|
|||
|
|
Uri.parse('https://posimai-nas.ts.net/api/recommendations'),
|
|||
|
|
body: jsonEncode({
|
|||
|
|
'target': {
|
|||
|
|
'prefecture': target.displayData.prefecture,
|
|||
|
|
'type': target.displayData.type,
|
|||
|
|
'tasteStats': target.hiddenSpecs.sakeTasteStats.toJson(),
|
|||
|
|
},
|
|||
|
|
'limit': limit,
|
|||
|
|
}),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// レスポンスをパース
|
|||
|
|
final data = jsonDecode(response.body);
|
|||
|
|
return data['recommendations']
|
|||
|
|
.map((json) => Recommendation.fromJson(json))
|
|||
|
|
.toList();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**効果**:
|
|||
|
|
- ✅ 自分のカードから5件 + 未知の銘柄から5件 = 計10件
|
|||
|
|
- ✅ ユーザーの探索欲求を満たす
|
|||
|
|
- ✅ アプリの価値向上
|
|||
|
|
|
|||
|
|
**実装工数**: 15時間
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**3. 日本酒マスターDBのデータ収集**
|
|||
|
|
|
|||
|
|
**データソース**:
|
|||
|
|
1. **ユーザー登録データ**:
|
|||
|
|
- 各ユーザーが登録した日本酒の平均五味チャート
|
|||
|
|
- 匿名化されたレビューデータ
|
|||
|
|
|
|||
|
|
2. **公開データ**:
|
|||
|
|
- 日本酒造組合中央会のデータ
|
|||
|
|
- 各酒蔵の公式Webサイト
|
|||
|
|
|
|||
|
|
3. **AI自動収集**:
|
|||
|
|
- Gemini APIで日本酒の情報を収集
|
|||
|
|
- 画像から五味チャートを推定
|
|||
|
|
|
|||
|
|
**データ量見積もり**:
|
|||
|
|
- 日本の日本酒銘柄数: **約10,000銘柄**
|
|||
|
|
- 各銘柄のデータサイズ: **1KB**
|
|||
|
|
- 合計データ量: **10MB**(軽量)
|
|||
|
|
|
|||
|
|
**実装工数**: 30時間
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### ⏳ Phase 3.0(リリース後3ヶ月)- 優先度: Medium
|
|||
|
|
|
|||
|
|
**4. ソーシャル機能との統合**
|
|||
|
|
|
|||
|
|
**機能**:
|
|||
|
|
- 友達が登録した銘柄を推薦
|
|||
|
|
- 「この銘柄を登録している人はこれも登録しています」
|
|||
|
|
- コミュニティの人気ランキング
|
|||
|
|
|
|||
|
|
**実装工数**: 40時間
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 実装優先度まとめ
|
|||
|
|
|
|||
|
|
| 項目 | 優先度 | 実装時期 | 工数 | 効果 |
|
|||
|
|
|------|--------|----------|------|------|
|
|||
|
|
| **問題1-1: 画像のメモリキャッシュ** | 🔴 Critical | Day 4 | 1時間 | 即座に改善 |
|
|||
|
|
| **問題1-2: サムネイル生成** | 🟠 High | Day 4-5 | 3時間 | 10倍高速化 |
|
|||
|
|
| **問題1-3: 遅延読み込み最適化** | 🟡 Medium | Day 5 | 1時間 | UX向上 |
|
|||
|
|
| **問題2-1: 共有DB構築** | 🟠 High | Phase 2.0 | 20時間 | 価値向上 |
|
|||
|
|
| **問題2-2: レコメンド拡張** | 🟠 High | Phase 2.0 | 15時間 | 探索欲求 |
|
|||
|
|
| **問題2-3: データ収集** | 🟡 Medium | Phase 2.0 | 30時間 | DB充実 |
|
|||
|
|
| **問題2-4: ソーシャル統合** | 🟢 Low | Phase 3.0 | 40時間 | 付加価値 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎯 Day 4の実装計画(修正版)
|
|||
|
|
|
|||
|
|
### 当初の計画
|
|||
|
|
- バッジ拡張(7個追加): 8時間
|
|||
|
|
|
|||
|
|
### 修正後の計画
|
|||
|
|
- **午前**: バッジ拡張(4時間)
|
|||
|
|
- **午後**: 画像パフォーマンス改善(4時間)
|
|||
|
|
- メモリキャッシュ実装(1時間)
|
|||
|
|
- サムネイル生成(3時間)
|
|||
|
|
|
|||
|
|
**合計**: 8時間(変更なし)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 💡 開発者への推奨アクション
|
|||
|
|
|
|||
|
|
### 今すぐ確認(5分)
|
|||
|
|
実機で以下を確認してください:
|
|||
|
|
|
|||
|
|
1. **画像ファイルサイズ**:
|
|||
|
|
```
|
|||
|
|
スマホ → ファイルマネージャー →
|
|||
|
|
Android/data/com.posimai.ponshu_room_lite/files/ →
|
|||
|
|
画像ファイルを確認
|
|||
|
|
```
|
|||
|
|
- 1枚あたりのファイルサイズは?
|
|||
|
|
- 何枚の画像がありますか?
|
|||
|
|
|
|||
|
|
2. **メモリ使用量**:
|
|||
|
|
- スマホの設定 → アプリ → ポンシュルーム → メモリ使用量
|
|||
|
|
|
|||
|
|
3. **カード数**:
|
|||
|
|
- 現在何枚の日本酒を登録していますか?
|
|||
|
|
|
|||
|
|
この情報があれば、より正確な対応策を提案できます。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**作成日**: 2026-01-22
|
|||
|
|
**作成者**: Cursor AI
|
|||
|
|
**更新予定**: Day 4実装後
|