何をするか?
Firestore はクライアントアプリから直接アクセスできることが利点ですが、多数のクライアントから複数のドキュメントを読み込んでいると、あっという間に無料枠を超えて高額な請求が発生してしまいます。
Firebase 8.2.0 でリリースされた Cloud Firestore Data Bundles という仕組みを使用すると、Firestore から取得したデータ(クエリ結果)をバンドルというデータにまとめておいて、それを使いまわすことができます。 データバンドルを CDN でキャッシュ、あるいはクライアントサイドでキャッシュすることにより、Firestore へのアクセスを発生させずに、あたかも Firestore からデータフェッチしたかのように動作させることが可能です。 ユーザー数の多いアプリに導入すれば、大きなコスト削減につながります。
データバンドルは Cloud Functions を使って作成してしまうのが簡単です。 下記のような構成にすれば、クライアントアプリは Firestore にアクセスする代わりにデータバンドルを取得して動作するようになります。
しかし、これだけでは複数のクライアントから Cloud Functions へのアクセスが発生してしまうので、結局はその都度 Firestore へのアクセスが発生してしまいます。
各クライアントアプリではキャッシュが有効ですが、そのキャッシュでさえ、Ctrl(Cmd) + R
によるスーパーリロードで無視されてしまいます。
そこで、次のようにさらに CDN (Firebase Hosting) を挟んでデータバンドルをキャッシュすることで、各クライアントからのアクセスで Cloud Functions が起動されてしまうのを防ぎます。
この構成になっていれば、クライアントがいくら強制リロードしようが、CDN (Firebase Hosting) にキャッシュされたデータバンドルのみが参照されるようになります。
Firestore へのアクセスが発生するのは、CDN 上のキャッシュが無効になったときのみです。
クライアント側のキャッシュ時間や、CDN のキャッシュ時間は、Cloud Functions の関数が返すレスポンスヘッダ (Cache-Control
) で制御できます。
Cloud Functions でデータバンドルを作成する
下記のコードでは、Cloud Functions に登録する createBundle
関数を定義しています。
処理の流れは次のようになっています。
- Firestore のコレクションからドキュメントを取得(ここでは最新の 50 件の books データ)
- データバンドルを作成し、上記 books データを名前付きクエリ結果として格納
- 関数のレスポンスとしてデータバンドルを返す
Firestore には既に books
コレクションが登録されているものとします。
ポイントは、CDN とクライアントアプリにデータバンドルをキャッシュさせるために Cache-Control
レスポンスヘッダを付加するところでしょうか(参考: Manage cache behavior | Firebase)。
response.set('Cache-Control', 'public, max-age=300, s-maxage=600')
Cache-Control
の各値は次のような意味を持っています。
public
… CDN (Firebase Hosting) でもデータバンドルをキャッシュさせます。これを指定しなかった場合のデフォルト値はprivate
で、クライアントアプリ(ブラウザ)のみがデータバンドルをキャッシュ可能になります。max-age=300
… キャッシュ有効時間(秒)を指示します。クライアントアプリ、および CDN がこの指示に従います。s-maxage=600
… CDN のキャッシュの有効時間(秒)を別途指示します。これを省略した場合は、CDN のキャッシュ有効時間にもmax-age
が使用されます。
つまり、クライアントアプリでデータバンドルを 300 秒間キャッシュに保持、CDN で 600 秒間キャッシュに保持することになります。
ホスティング動作を構成する | Firebase Documentation のドキュメントで、CDN (Firebase Hosting) をプロキシさせて Cloud Functions を呼び出す場合は、us-central1
のみサポートされているとの記載があります(2022-06 現在)。
重要: Firebase Hosting は、us-central1 でのみ Cloud Functions をサポートします。
たしかに、asia-northeast1
に Cloud Functions 関数をデプロイしてしまうと、CDN との連携がうまくいかない(問答無用で us-central1
の Cloud Functions に転送されてしまう)ので、今はあきらめて us-central1
にデプロイしておくしかなさそうです。
Google さん、対応してね!
CDN (Firebase Hosting) でデータバンドルをキャッシュする
Firestore のデータバンドルをキャッシュするための CDN (Firebase Hosting) は簡単に作成することができます。
firebase init functions コマンドなどで Firebase プロジェクトを作成 している場合、プロジェクトのルートディレクトリに firebase.json
ファイルが生成されているはずです。
このファイルに次のように追記することで、Cloud Functions へのプロキシとなる CDN を配置できます。
設定内容はシンプルで、/createBundle
という URL へのアクセスを、Cloud Functions の createBundle
関数の呼び出しに接続しています。
さらに、別ドメインに配置した Web アプリから CDN へのアクセスを許可するために、CORS 用のレスポンスヘッダ定義を追加しておく必要があります。
Cloud Functions 関数の実装と、CDN の設定が済んだら、プロジェクトルートで下記コマンドを実行してデプロイします。
$ firebase deploy
デプロイに成功すると、Cloud Functions と CDN の次のようなエンドポイントが有効になります。
- Cloud Functions:
https://us-central1-myapp-12345.cloudfunctions.net/createBundle
- CDN (Firestore Hosting):
https://myapp-12345.web.app/createBundle
どちらも同じデータバンドルを返しますが、CDN の方はキャッシュ生成後は一瞬でデータを返してくれるはずです。
クライアントアプリからデータバンドルを取得する
最後に、CDN から Firestore データバンドルを取得するクライアントアプリ側の実装です。 Firebase SDK の基本的な扱い方は省略します(参考: Next.js で Firebase: Cloud Firestore データベースを使う)。
例えば、React のカスタムフックなどから上記の関数を呼び出すことで、データバンドルをもとに生成された Book
配列を取得できます。
CDN のキャッシュデータだけを参照するので、直接 Firestore にアクセスするも高速に動作し、かつ安価に運用できます。
もちろん、データ取得の柔軟性は減りますし、キャッシュ期間中は最新データを取得できないといった制約がありますが、多数のユーザーがアクセスするトップページの情報などは、この仕組みを導入する価値がありそうです。