CDN からバンドル Firestore コンテンツを配信する

多くのアプリケーションにおいて、初めてのページ読み込みではすべてのユーザーに対して同じコンテンツが配信されます。たとえば、ニュースサイトで最新の記事を表示したり、e コマースサイトでベストセラー商品を表示したりできます。

このコンテンツが Cloud Firestore から配信される場合、各ユーザーがアプリケーションを読み込むと、同じ結果に対して新しいクエリが実行されます。これらの結果は、ユーザー間ではキャッシュに保存されないため、アプリケーションの処理速度が低下し、必要以上にコストが高くなります。

ソリューション: バンドル

Cloud Firestore バンドルを使用すると、Firebase Admin SDK を使用してバックエンドの共通クエリ結果からデータバンドルを作成し、CDN でキャッシュに保存された計算済みの blob を提供できます。これにより、ユーザーの最初の読み込みにかかる時間が大幅に短くなり、Cloud Firestore のクエリコストを削減できます。

このガイドでは、Cloud Functions を使用してバンドルを生成し、Firebase Hosting を使用してバンドルのコンテンツを動的にキャッシュに保存して配信します。バンドルの詳細については、ガイドをご覧ください。

最初に、最新の 50 件の「ストーリー」をクエリし、結果をバンドルとして配信する簡単な公開 HTTP 関数を作成します。

Node.js
exports.createBundle = functions.https.onRequest(async (request, response) => {
  // Query the 50 latest stories
  const latestStories = await db.collection('stories')
    .orderBy('timestamp', 'desc')
    .limit(50)
    .get();

  // Build the bundle from the query results
  const bundleBuffer = db.bundle('latest-stories')
    .add('latest-stories-query', latestStories)
    .build();

  // Cache the response for up to 5 minutes;
  // see https://firebase.google.com/docs/hosting/manage-cache
  response.set('Cache-Control', 'public, max-age=300, s-maxage=600');

  response.end(bundleBuffer);
});
      
Java

package com.example;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreBundle;
import com.google.cloud.firestore.Query.Direction;
import com.google.cloud.firestore.QuerySnapshot;
import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.cloud.FirestoreClient;
import java.io.BufferedWriter;
import java.io.IOException;

public class ExampleFunction implements HttpFunction {

  public static FirebaseApp initializeFirebase() throws IOException {
    if (FirebaseApp.getApps().isEmpty()) {
      FirebaseOptions options = FirebaseOptions.builder()
          .setCredentials(GoogleCredentials.getApplicationDefault())
          .setProjectId("YOUR-PROJECT-ID")
          .build();

      FirebaseApp.initializeApp(options);
    }

    return FirebaseApp.getInstance();
  }

  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    // Get a Firestore instance
    FirebaseApp app = initializeFirebase();
    Firestore db = FirestoreClient.getFirestore(app);

    // Query the 50 latest stories
    QuerySnapshot latestStories = db.collection("stories")
        .orderBy("timestamp", Direction.DESCENDING)
        .limit(50)
        .get()
        .get();

    // Build the bundle from the query results
    FirestoreBundle bundle = db.bundleBuilder("latest-stores")
        .add("latest-stories-query", latestStories)
        .build();

    // Cache the response for up to 5 minutes
    // see https://firebase.google.com/docs/hosting/manage-cache
    response.appendHeader("Cache-Control", "public, max-age=300, s-maxage=600");

    // Write the bundle to the HTTP response
    BufferedWriter writer = response.getWriter();
    writer.write(new String(bundle.toByteBuffer().array()));
  }
}
      

次に firebase.json を変更して、この Cloud Functions の関数を配信およびキャッシュに保存するように Firebase Hosting を構成します。この構成では、Firebase Hosting CDN は、Cloud Functions の関数によって設定されたキャッシュ設定に基づいてバンドルのコンテンツを配信します。キャッシュが期限切れになると、関数を再度トリガーしてコンテンツを更新します。

firebase.json
{
  "hosting": {
    // ...
    "rewrites": [{
      "source": "/createBundle",
      "function": "createBundle"
    }]
  },
  // ...
}

最後にウェブ アプリケーションで、CDN からバンドル コンテンツを取得して Firestore SDK に読み込みます。

// If you are using module bundlers.
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/firestore/bundle" // This line enables bundle loading as a side effect.

async function fetchFromBundle() {
  // Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache'
  // response header will be set to 'HIT'
  const resp = await fetch('/createBundle');

  // Load the bundle contents into the Firestore SDK
  await db.loadBundle(resp.body);

  // Query the results from the cache
  // Note: omitting "source: cache" will query the Firestore backend.
  
  const query = await db.namedQuery('latest-stories-query');
  const storiesSnap = await query.get({ source: 'cache' });

  // Use the results
  // ...
}

コスト削減額の見積もり

1 日あたりユーザー 100,000 人がアクセスし、各ユーザーが初回読み込み時に同一の 50 件のトップニュースを読み込むニュース ウェブサイトがあるとします。キャッシュを使用しない場合、Cloud Firestore から 1 日あたり 50 x 100,000 = 5,000,000 回のドキュメントの読み取りが行われます。

このサイトで上記の手法を取り入れ、50 件の結果を最大 5 分間キャッシュに保存するとします。すると、ユーザーごとにクエリ結果を読み込む代わりに、結果の読み込みが 1 時間あたり 12 回実行されるようになります。サイトにアクセスするユーザーの数に関係なく、Cloud Firestore に対するクエリの数は同じままです。このページでは、ドキュメントの読み取り回数が 5,000,000 回ではなく、1 日あたり 12 x 24 x 50 = 14,400 回となります。Firebase Hosting と Cloud Functions を使用することで生じるわずかな追加コストは、Cloud Firestore によるコスト削減によって相殺されます。

デベロッパーにもコスト削減のメリットがありますが、最大のメリットを得るのはユーザーです。この 50 件のドキュメントを、Cloud Firestore から直接読み込むのではく、Firebase Hosting CDN から読み込むことで、ページのコンテンツ読み込み時間を容易に 100~200 ミリ秒以上短縮できます。読み込みの速いページがユーザーの満足度の向上につながることが、調査で繰り返し明らかになっています。