Gebündelte Firestore-Inhalte aus einem CDN bereitstellen

Viele Anwendungen stellen allen Nutzern beim ersten Seitenaufbau dieselben Inhalte bereit. Auf einer Nachrichtenseite können beispielsweise die neuesten Artikel oder auf einer E-Commerce-Website die meistverkauften Artikel angezeigt werden.

Wenn diese Inhalte von Cloud Firestore bereitgestellt werden, sendet jeder Nutzer beim Laden der Anwendung eine neue Abfrage für dieselben Ergebnisse. Da diese Ergebnisse nicht zwischen Nutzern im Cache gespeichert werden, ist die Anwendung langsamer und teurer als nötig.

Lösung: Bundles

Mit Cloud Firestore-Bundles können Sie Daten-Bundles aus gängigen Abfrageergebnissen im Backend mit dem Firebase Admin SDK zusammenstellen und diese vorab berechneten Blobs, die in einem CDN im Cache gespeichert sind, bereitstellen. Dadurch wird die erste Ladezeit für Ihre Nutzer erheblich verkürzt und Ihre Cloud Firestore-Abfragekosten werden reduziert.

In dieser Anleitung verwenden wir Cloud Functions, um Bundles zu generieren, und Firebase Hosting, um Bundle-Inhalte dynamisch im Cache zu speichern und bereitzustellen. Weitere Informationen zu Bundles finden Sie in den Anleitungen.

Erstellen Sie zuerst eine einfache öffentliche HTTP-Funktion, um die 50 neuesten „Storys“ abzufragen und das Ergebnis als Bundle bereitzustellen.

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

Konfigurieren Sie als Nächstes Firebase Hosting, um diese Cloud Functions-Funktion bereitzustellen und im Cache zu speichern. Ändern Sie dazu firebase.json. Bei dieser Konfiguration stellt das Firebase Hosting-CDN den Bundle-Inhalt gemäß den von der Cloud Functions-Funktion festgelegten Cache-Einstellungen bereit. Wenn der Cache abläuft, wird der Inhalt aktualisiert, indem die Funktion noch einmal ausgelöst wird.

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

Rufen Sie schließlich in Ihrer Webanwendung die gebündelten Inhalte aus dem CDN ab und laden Sie sie in das 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
  // ...
}

Geschätzte Einsparung

Stellen Sie sich eine Nachrichtenwebsite vor,die täglich 100.000 Nutzer hat und bei der jeder Nutzer beim ersten Laden die gleichen 50 Top-Storys lädt. Ohne Caching würde dies zu 50 × 100.000 = 5.000.000 Dokumentlesevorgängen pro Tag aus Cloud Firestore führen.

Angenommen, die Website übernimmt die oben beschriebene Technik und speichert diese 50 Ergebnisse bis zu 5 Minuten lang im Cache. Anstatt die Abfrageergebnisse für jeden Nutzer zu laden, werden die Ergebnisse also genau 12-mal pro Stunde geladen. Unabhängig davon, wie viele Nutzer die Website besuchen, bleibt die Anzahl der Abfragen an Cloud Firestore gleich. Anstelle von 5.000.000 Dokumentlesevorgängen würde diese Seite 12 x 24 x 50 = 14.400 Dokumentlesevorgänge pro Tag verwenden. Die geringen zusätzlichen Kosten für Firebase Hosting und Cloud Functions werden durch die Kosteneinsparungen bei Cloud Firestore leicht ausgeglichen.

Der Entwickler profitiert zwar von den Kosteneinsparungen, der größte Nutznießer ist jedoch der Nutzer. Wenn Sie diese 50 Dokumente aus dem Firebase Hosting-CDN statt direkt aus Cloud Firestore laden, kann sich die Ladezeit der Inhalte auf der Seite um 100–200 ms oder mehr verkürzen. Studien haben wiederholt gezeigt, dass schnelle Seiten zu zufriedeneren Nutzern führen.