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

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

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

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

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

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

קודם יוצרים פונקציית 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 Hosting במקום ישירות מ-Cloud Firestore יכולה לקצר את זמן טעינת התוכן של הדף ב-100 עד 200 אלפיות השנייה או יותר. מחקרים הראו שוב ושוב שדפים מהירים משמחים את המשתמשים.