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

Wiele aplikacji wyświetla tę samą treść wszystkim użytkownikom przy pierwszym wczytaniu strony. Dla: np. witryna z wiadomościami może przedstawiać najnowsze artykuły, a witryna e-commerce – może pokazywać 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ą przesyłane między użytkownikami w pamięci podręcznej, aplikacja działa wolniej i działa jest znacznie droższe.

Rozwiązanie: pakiety

Pakiety Cloud Firestore umożliwiają złożenie pakietów danych z powszechnie używanych wyników zapytań na zapleczu za pomocą pakietu Firebase Admin SDK i przechowywanie tych wyprzedżętnie obliczonych plików w pamięci podręcznej CDN. Dzięki temu użytkownicy będą mogli znacznie szybciej wczytywać treści, a Ty zaoszczędzisz na kosztach 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 przesłać zapytanie dotyczące 50 najnowszych „artykułów” oraz i wyświetlać wynik w formie pakietu.

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ę w chmurze. 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"
    }]
  },
  // ...
}

Na koniec w aplikacji internetowej pobierz pakiety treści z sieci CDN i wczytaj 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 przyjmuje powyższą metodę i zapisuje w pamięci podręcznej 50 wyników do 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 000 000 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.

Chociaż deweloper czerpie korzyści z oszczędności, największym jego korzyścią jest użytkownika. Wczytywanie tych 50 dokumentów z sieci CDN Hostingu Firebase, a nie bezpośrednio z urządzenia Cloud Firestore można z łatwością skrócić o 100–200 ms czas wczytywania treści strony. Badania wielokrotnie wykazały, że szybsze strony sprawiają, że użytkownicy są zadowoleni.