הגשת תוכן מ-CDN בחבילה

אפליקציות רבות מציגות תוכן זהה לכל המשתמשים בטעינת הדף הראשון. לדוגמה, באתר חדשות יכולות להופיע הכתבות האחרונות, ובאתר מסחר אלקטרוני יכולים להופיע הפריטים הנמכרים ביותר.

אם התוכן הזה יוצג מ-Cloud Firestore, כל משתמש ישלח שאילתה חדשה לקבלת אותן תוצאות כשיריד את האפליקציה. מכיוון שהתוצאות האלה לא נשמרות במטמון בין משתמשים, האפליקציה איטית ויותר יקרה מהצורך.

פתרון: חבילות

חבילות Cloud Firestore מאפשרות לכם לאסוף חבילות נתונים מתוצאות שאילתות נפוצות בקצה העורפי באמצעות Firebase Admin SDK, ולהציג את ה-blobs המחושב מראש ששמורים במטמון ב-CDN. כך המשתמשים ייהנו מחוויית טעינה ראשונית מהירה יותר, והעלויות של שאילתות Cloud Firestore יפחתו.

במדריך הזה נשתמש ב-Cloud Functions כדי ליצור חבילות וב-Firebase Hosting כדי לשמור ב-cache ולספק באופן דינמי את התוכן של החבילות. מידע נוסף על חבילות זמין במדריכים.

קודם כול יוצרים פונקציית 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()));
  }
}
      

בשלב הבא, משנים את הערך של firebase.json כדי להגדיר את Firebase Hosting להצגת הפונקציה הזו של Cloud Functions ולשמירה שלה במטמון. בהגדרה הזו, רשת ה-CDN של Firebase Hosting תציג את תוכן החבילה בהתאם להגדרות המטמון שהוגדרו על ידי Cloud Function. כשפג התוקף של המטמון, הפונקציה מופעלת שוב כדי לרענן את התוכן.

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

אומדן החיסכון

נניח שאתר חדשות מקבל 100,000 משתמשים ביום, וכל משתמש טוען את אותן 50 הכתבות המובילות בעומס הראשוני. ללא שמירת מטמון, המשמעות היא 50 x 100,000 = 5,000,000 קריאות למסמכים ביום מ-Cloud Firestore.

עכשיו נניח שהאתר משתמש בשיטה שלמעלה וש-50 התוצאות האלה מאוחסנות במטמון למשך עד 5 דקות. כך, במקום לטעון את תוצאות השאילתה לכל משתמש, התוצאות נטענות בדיוק 12 פעמים בשעה. לא משנה כמה משתמשים מגיעים לאתר, מספר השאילתות ל-Cloud Firestore נשאר ללא שינוי. במקום 5,000,000 קריאות מסמכים, הדף הזה ישתמש ב-12 x 24 x 50 = 14,400 קריאות מסמכים ביום. העלויות הנוספות הקטנות של Firebase Hosting ו-Cloud Functions מתאזנות בקלות בחיסכון בעלויות של Cloud Firestore.

המפתח מפיק תועלת מהחיסכון בעלויות, אבל המוטב הגדול ביותר הוא המשתמש. טעינה של 50 המסמכים האלה מ-CDN של מארח Firebase במקום מ-Cloud Firestore ישירות יכולה לקצר את זמן הטעינה של 100-200 אלפיות השנייה או יותר מזמן טעינת התוכן של הדף. מחקרים הראו שוב ושוב שדפים מהירים הם משתמשים מרוצים יותר.