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

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

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

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

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

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

קודם כל, יוצרים פונקציית HTTP ציבורית פשוטה כדי לשלוח שאילתה על 50 ה-"stories" האחרונים ולהציג את התוצאה כחבילה.

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 כך שיציג את הפונקציה הזו של Cloud Functions וישמור אותה במטמון. בהגדרה הזו, רשת ה-CDN של Firebase Hosting תציג את תוכן החבילה בהתאם להגדרות המטמון שנקבעו על ידי הפונקציה של Cloud Functions. כשתוקף המטמון יפוג, התוכן יתרענן על ידי הפעלת הפונקציה מחדש.

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 כפול 100,000, כלומר 5,000,000 קריאות של מסמכים ביום מ-Cloud Firestore.

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

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