Join us for Firebase Summit on November 10, 2021. Tune in to learn how Firebase can help you accelerate app development, release with confidence, and scale with ease. Register

从 CDN 提供捆绑的 Firestore 内容

许多应用程序在第一页加载时为所有用户提供相同的内容。例如,一个新闻网站可能会展示最新的故事,或者一个电子商务网站可能会展示最畅销的商品。

如果此内容是从 Cloud Firestore 提供的,则每个用户都会在加载应用程序时针对相同的结果发出新查询。因为这些结果不会在用户之间缓存,所以应用程序比它需要的更慢和更昂贵。

解决方案:捆绑

Cloud Firestore 包允许您使用 Firebase Admin SDK 从后端的常见查询结果中组合数据包,并提供这些缓存在 CDN 上的预计算 blob。这可为您的用户提供更快的首次加载体验,并降低您的 Cloud Firestore 查询成本。

在本指南中,我们将使用 Cloud Functions 生成包和 Firebase 托管来动态缓存和提供包内容。关于捆绑的更多信息是可用的指南

首先创建一个简单的公共 HTTP 函数来查询 50 个最新的“故事”并将结果作为一个包提供。

节点.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);
});
      
爪哇

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 托管 CDN 将根据 Cloud Function 设置的缓存设置提供捆绑内容。当缓存过期时,它将通过再次触发该功能来刷新内容。

firebase.json
{
  "hosting": {
    // ...
    "rewrites": [{
      "source": "/createBundle",
      "function": "createBundle"
    }]
  },
  // ...
}

最后,在您的 Web 应用程序中,从 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 Hosting CDN 而不是直接从 Cloud Firestore 加载这 50 个文档可以轻松地将页面内容加载时间缩短 100-200 毫秒或更多。研究一再表明,快速的页面意味着更快乐的用户。