透過 CDN 提供隨附的 Firestore 內容

許多應用程式會在第一次載入網頁時,為所有使用者提供相同的內容。舉例來說,新聞網站可能會顯示最新的新聞,而電子商務網站則可能會顯示暢銷商品。

如果這項內容是從 Cloud Firestore 提供,每位使用者在載入應用程式時,都會針對相同的結果發出新的查詢。由於這些結果不會在使用者之間快取,因此應用程式會變得較慢,且成本也較高。

解決方案:組合

Cloud Firestore 組合可讓您使用 Firebase Admin SDK 組合後端中常見查詢結果的資料組合,並在 CDN 上提供這些預先運算的 blob。這麼做可為使用者提供更快速的首次載入體驗,並降低 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 託管,修改 firebase.json 來提供和快取這個 Cloud 函式。有了這個設定,Firebase Hosting CDN 就會根據 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 則熱門新聞。在未設定任何快取的情況下,這會導致 Cloud Firestore 每天有 50 x 100,000 = 5,000,000 次文件讀取。

假設網站採用上述技巧,並將這 50 個結果快取至最多 5 分鐘。因此,結果是每小時完全載入 12 次,而不是為每位使用者載入查詢結果。無論有多少使用者抵達網站,對 Cloud Firestore 的查詢數量都不會改變。這個頁面會使用 12 x 24 x 50 = 14,400 次/天的文件讀取次數,而非 5,000,000 次。Firebase 託管和 Cloud Functions 的額外費用很少,因此您可以輕鬆抵銷 Cloud Firestore 的節省費用。

雖然開發人員可從節省的成本中受益,但使用者才是最大受益者。從 Firebase 託管 CDN 載入這 50 份文件 (而不是直接從 Cloud Firestore 載入),很容易就網頁的內容載入時間造成 100 到 200 毫秒以上的時間。研究不斷指出,快速載入的網頁 對使用者而言更滿意