Join us for Firebase Summit on November 10, 2021. Tune in to learn how Firebase can help you accelerate app development, release with confidence, and scale with ease. Register

CDNからバンドルされたFirestoreコンテンツを提供する

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

このコンテンツがCloudFirestoreから提供される場合、各ユーザーは、アプリケーションをロードするときに同じ結果に対して新しいクエリを発行します。これらの結果はユーザー間でキャッシュされないため、アプリケーションは必要以上に遅く、高価になります。

解決策:バンドル

Cloud Firestoreバンドルを使用すると、Firebase Admin SDKを使用してバックエンドの一般的なクエリ結果からデータバンドルをアセンブルし、CDNにキャッシュされたこれらの事前計算されたBLOBを提供できます。これにより、ユーザーの初回読み込みエクスペリエンスが大幅に高速化され、CloudFirestoreクエリのコストが削減されます。

このガイドでは、Cloud Functionsを使用してバンドルを生成し、FirebaseHostingを使用してバンドルコンテンツを動的にキャッシュして提供します。バンドルの詳細についてはで利用可能であるガイド

まず、単純なパブリックHTTP関数を作成して、最新の50個の「ストーリー」をクエリし、その結果をバンドルとして提供します。

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()));
  }
}
      

次のconfigure Firebaseを変更することで、このクラウド機能を提供してキャッシュするためにホスティングfirebase.json 。この構成では、Firebase Hosting CDNは、クラウド機能によって設定されたキャッシュ設定に従ってバンドルコンテンツを提供します。キャッシュの有効期限が切れると、関数を再度トリガーしてコンテンツを更新します。

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

最後に、Webアプリケーションで、バンドルされたコンテンツをCDNからフェッチし、FirestoreSDKにロードします。

// 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のトップストーリーを読み込むニュースWebサイトについて考えてみます。キャッシュがないと、CloudFirestoreから1日あたり50x 100,000 = 5,000,000のドキュメント読み取りが発生します。

ここで、サイトが上記の手法を採用し、それらの50件の結果を最大5分間キャッシュするとします。したがって、すべてのユーザーのクエリ結果をロードする代わりに、結果は1時間に正確に12回ロードされます。サイトにアクセスするユーザーの数に関係なく、CloudFirestoreへのクエリの数は同じままです。このページでは、5,000,000回のドキュメント読み取りの代わりに、1日あたり12 x 24 x 50 = 14,400回のドキュメント読み取りを使用します。 FirebaseHostingとCloudFunctionsのわずかな追加コストは、CloudFirestoreのコスト削減によって簡単に相殺されます。

開発者はコスト削減の恩恵を受けますが、最大の受益者はユーザーです。 CloudFirestoreから直接ではなくFirebaseHosting CDNからこれらの50のドキュメントを読み込むと、ページのコンテンツの読み込み時間から100〜200ミリ秒以上を簡単に削減できます。調査によると、スピーディーなページはユーザーの満足度を高めることが繰り返し示されています。