Obsługa połączonych treści Firestore z CDN

Wiele aplikacji wyświetla te same treści wszystkim użytkownikom przy pierwszym wczytaniu strony. Na przykład witryna z wiadomościami może wyświetlać najnowsze artykuły, a sklep internetowy – najlepiej sprzedające się produkty.

Jeśli te treści są wyświetlane z poziomu Cloud Firestore, każdy użytkownik będzie wysyłać nowe zapytanie o te same wyniki po załadowaniu aplikacji. Ponieważ te wyniki nie są przechowywane w pamięci podręcznej między użytkownikami, aplikacja działa wolniej i jest droższa, niż powinna.

Rozwiązanie: pakiety

Pakiety Cloud Firestore umożliwiają złożenie pakietów danych z powszechnych wyników zapytań na zapleczu za pomocą pakietu Firebase Admin SDK i przechowywanie tych wyprzedżętnie obliczonych blobów w pamięci podręcznej w CDN. Dzięki temu użytkownicy mogą znacznie szybciej wczytać treści, a Ty obniżysz koszty zapytań Cloud Firestore.

W tym przewodniku użyjemy funkcji Cloud Functions do generowania pakietów oraz funkcji Firebase Hosting do dynamicznego buforowania i wyświetlania treści pakietu. Więcej informacji o pakietach znajdziesz w przewodnikach.

Najpierw utwórz prostą publiczną funkcję HTTP, aby zapytać o 50 najnowszych „stories” i zwrócić wynik jako pakiet.

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

Następnie skonfiguruj Hosting Firebase, aby serwować i przechowywać w pamięci podręcznej tę funkcję Cloud Functions, modyfikując plik firebase.json. W tej konfiguracji sieć Firebase Hosting CDN będzie wyświetlać zawartość pakietu zgodnie z ustawieniami pamięci podręcznej określonymi przez funkcję Cloud. Gdy pamięć podręczna wygaśnie, funkcja zostanie ponownie wywołana, aby odświeżyć treści.

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

W swojej aplikacji internetowej pobierz zawartość pakietu z CDN i załaduj ją do pakietu SDK Firestore.

// 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
  // ...
}

Szacowane oszczędności

Weźmy pod uwagę witrynę z wiadomościami, która ma 100 tys. użytkowników dziennie, a każdy z nich wczytuje te same 50 najpopularniejszych artykułów podczas pierwszego załadowania. Bez buforowania oznaczałoby to 50 x 100 000 = 5 000 000 odczytów dokumentów Cloud Firestore dziennie.

Załóżmy teraz, że witryna stosuje opisaną wyżej technikę i przechowuje w pamięci podręcznej te 50 wyników przez maksymalnie 5 minut. Zamiast wczytywać wyniki zapytań dla każdego użytkownika, wyniki są wczytywane dokładnie 12 razy na godzinę. Niezależnie od tego, ilu użytkowników odwiedza witrynę, liczba zapytań do Cloud Firestore pozostaje taka sama. Zamiast 5 mln odczytów dokumentów ta strona wykorzysta 12 x 24 x 50 = 14 400 odczytów dokumentów dziennie. Niewielkie dodatkowe koszty związane z Cloud Functions i Hostingiem Firebase są łatwo rekompensowane przez oszczędności Cloud Firestore.

Deweloper korzysta na oszczędnościach, ale największym beneficjentem jest użytkownik. Wczytywanie tych 50 dokumentów z CDN Hostingu Firebase, a nie bezpośrednio z Cloud Firestore, może skrócić czas wczytywania treści strony o 100–200 ms lub więcej. Badania wielokrotnie wykazały, że szybsze strony sprawiają, że użytkownicy są zadowoleni.