💰

個人開発はなんとかして無料枠で運用したい

に公開

はじめに

個人開発でリリースしたモバイルパズルゲーム「ジャマイカの木」で実際に検討したコスト最適化手法をご紹介します。

Firebase無料枠で運用できるように抑えつつ、パフォーマンスや将来的な拡張性を大きく損なわないように意識しました。

ソフトウェア設計としては基本の考え方なので、Firebase以外にも応用できると思います。

Vibe Codingで任せきっていると考慮漏れがちなポイントなので、ぜひご一読ください。

開発したアプリ「ジャマイカの木」

本題に入る前に今回開発したアプリ「ジャマイカの木」について簡単に説明します。

今回リリースした「ジャマイカの木」は5つの数字と四則演算を使って目標の数を作るパズルゲームです。

まずは完成品を見てもらうのが一番早いと思います。ぜひダウンロードしてみてください。

https://apps.apple.com/jp/app/ジャマイカの木-数字をつなげる計算パズル/id6749314618

ちなみに開発の流れなどはこちらでまとめています。

https://zenn.dev/jun_t/articles/62fc3b55bbabed

スコアランキング機能の基本設計

ジャマイカの木では、スコア画面でみんなのスコアを確認することができます。
ここにはユーザのスコアランキングTOP10が表示されています。TOP10までしか表示されていないという点が重要です。

ジャマイカの木ではこのスコア管理のバックエンドとしてFirabaseを採用していますので、本記事ではこの機能に特に注目してご紹介します。

データ構造の検討

Firebase 無料枠でのランキングシステム実装にあたり、2つの主要なアプローチを検討しました。

パターン A: TOP10のみを保存する方式

1つ目はスコアのTOP10のみを保存する方式です。TOP10のみしか保存しないのだから、それだけ保存しておけば十分だろうというシンプルな考え方です。

データ構造例(rankings)
// Firestoreコレクション: rankings
// ドキュメント数: 最大10件(難易度ごと)

{
  "rank": 1,
  "userId": "user_123456",
  "displayName": "プレイヤー太郎",
  "score": 5000,
  "timestamp": 1704067200000
},
{
  "rank": 2,
  "userId": "user_789012",
  "displayName": "ゲーマー花子",
  "score": 4800,
  "timestamp": 1704153600000
}
// ... rank_3 〜 rank_10 まで

この方法のメリットは、なんといってもストレージコストです。どれだけユーザ数が増えたとしても、Firebaseで管理するデータは上位10人分のスコアのみで済みます。なんとしても無料枠で抑えたいという要望を叶えてくれそうですね。ランキング表示時はデータをそのまま引っ張ってきて表示するだけという点も処理がシンプルでいいですね。

一方でデメリットもいくつかあります。特に気になるのは更新処理の複雑さ拡張性の低さです。ランキング更新のためには「新スコアがTOP10に入るかどうか」を判定して更新する処理が必要になりますが、複数ユーザーが同時にスコア更新を行う場合の競合状態を適切に処理する必要があります。また、例えば「自分の順位はTOP10圏外でも常に表示したい」のような要件に対応できないなど拡張性に欠けます。

パターン B: 全ユーザーハイスコア保存方式(採用)

2つ目は、各ユーザーのスコアを独立したドキュメントで管理する方法です。パターンAのようにランキング形式で管理しているわけではないので、ランキング表示のためにはソートして上位10件のみ取得するようなクエリが必要になります。

データ構造例(scores)
// Firestoreコレクション: scores
// ドキュメント数: ユーザー数と同じ

{
  "userId": "user_123456",
  "displayName": "プレイヤー太郎",
  "score": 5000,
  "timestamp": 1704067200000
},
{
  "userId": "user_123457",
  "displayName": "ゲーマー花子",
  "score": 5000,
  "timestamp": 1704067200000
}
// ... 〜 user_xxxxxx

この方法のメリットとしては、同じレコードを更新するということが起こりえないため更新処理がシンプルになり、考えることを大幅に削減できます。また、「自分の順位」や「上位○%」といったユーザー体験向上に必要な情報を容易に提供できる拡張性も重要な優位性です。

設計パターン比較

それぞれの設計思想と特徴を、コスト・更新速度・参照速度・拡張性の4つの軸で比較検討した結果を以下に示します。

評価軸 パターンA: TOP10のみ保存 パターンB: 全ユーザー保存(採用)
コスト ◎ 最小(固定10×難易度数) △ ユーザー数に比例増加
更新 × 複雑な判定・排他制御 ◎ 単純な個別更新
参照 ◎ 直接取得 △ 取得時にソートが必要
拡張性 × 機能追加が困難 ◎ 柔軟な機能追加が可能

コストや拡張性のデメリットの方が大きいと判断し、パターンBを採用しました。

スケーラビリティ対策

採用したパターンBには、参照の観点でユーザー数増加時の懸念がありますので、念のため将来的なスケールを見据えた対策も検討しておきます。

今回は以下のような定期集計によるランキングキャッシュテーブルを設計しました。

データ構造例(rankings)
// 別途 rankings コレクションを作成
// Cloud Functions等で定期的に更新(例:1時間ごと)

{
  "updatedAt": 1704358800000,
  "top10": [
    { "rank": 1, "userId": "user_123", "displayName": "太郎", "score": 5000 },
    { "rank": 2, "userId": "user_456", "displayName": "花子", "score": 4800 },
    // ... top 10
  ]
}

この方式では、参照速度をO(1)まで向上させることができ、読み取りコストも大幅に削減できます。ただし、リアルタイム性とのトレードオフが発生するため、アプリケーションの性質に応じた判断が必要です。

コスト最適化の工夫

更新頻度の最適化

Firebaseでは更新頻度や参照頻度に制限があるため、条件付き更新により更新頻度を削減しました。

条件付き更新フローチャート

参照処理の最適化

キャッシュ活用により参照も同様に頻度を削減します。

3 層のデータ管理戦略

  1. Firestore(真実の源)

    • 全ユーザーの最新ハイスコア
    • リアルタイムランキング
  2. メモリキャッシュ(アプリ内)

    • ランキングデータの一時保存
    • TTL: 5 分
  3. AsyncStorage(永続化)

    • 自分のハイスコア
    • オフライン時の表示用

データ取得フローの最適化

3層キャッシュ戦略のフロー図

データ取得戦略の詳細

  • AsyncStorage: 自分のスコアを即座に表示(オフライン対応)
  • メモリキャッシュ: ランキングデータの一時保存(TTL: 5分)
  • Firestore: 最新ランキングの非同期取得(インデックスクエリ)

この3層構成により、初回表示の高速化とネットワーク負荷の軽減を両立しています。

まとめ

今回Claude Codeで開発しましたが、この辺りのコスト最適化は残念ながら指示しないとやってくれませんでした。

「気づいたらコスパの悪い設計で開発・リリースしてしまっていた」とならないように、この辺りは最低限人間がチェックした方が良さそうですね。

もし少しでも興味を持っていただけた方は「ジャマイカの木」で遊んでみてください。

そしてさらにお時間があればフィードバックやApp Storeのレビューをいただけると嬉しいです…!(まだまだ改善点やスマホアプリならではの考慮できていない点が多々ありそうなので…)

https://apps.apple.com/jp/app/ジャマイカの木-数字をつなげる計算パズル/id6749314618

Discussion