Entregar contenido de un conjunto de Firestore mediante una CDN

En muchas aplicaciones, se entrega el mismo contenido a todos los usuarios cuando se carga la primera página. Por ejemplo, en un sitio de noticias podrían mostrarse las noticias más recientes, y, en un sitio de comercio electrónico, los artículos más vendidos.

Si el contenido se entrega desde Cloud Firestore, cada usuario enviará una consulta nueva para los mismos resultados cuando se cargue la aplicación. Como los resultados no se almacenan en caché entre los usuarios, la app es más lenta y genera más gastos de los que corresponden.

La solución son los conjuntos

Los conjuntos de Cloud Firestore te permiten crear conjuntos de datos a partir de los resultados de consultas comunes en el backend. Para ello, usa el SDK de Firebase Admin y entrega BLOB preprocesados que se almacenan en la caché de una CDN. Esto permite que los usuarios tengan una experiencia de primera carga mucho más rápida y, al mismo tiempo, reduce tus costos de consultas de Cloud Firestore.

En esta guía, usaremos Cloud Functions para generar paquetes y Firebase Hosting para almacenar en caché y entregar contenido del paquete de forma dinámica. Obtén más información sobre los conjuntos en las guías.

Primero, crea una función de HTTP pública simple para consultar las 50 “noticias” más recientes y mostrar el resultado como un conjunto.

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()));
  }
}
      

Luego, modifica firebase.json y configura Firebase Hosting para que entregue y almacene en caché esta función de Cloud Functions. Con esta configuración, la CDN de Firebase Hosting entregará el contenido del conjunto según la configuración de la caché que se estableció en la Cloud Function. Después de que venza la caché, se actualizará el contenido reactivando la función.

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

Por último, en tu aplicación web, recupera el contenido del conjunto de la CDN y cárgalo en el SDK de Firestore.

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

Ahorro estimado

Piensa en un sitio web de noticias con 100,000 usuarios por día. Cada usuario ve los mismos 50 artículos principales en la carga inicial. Sin el almacenamiento en caché, esto generaría 5,000,000 de operaciones de lectura de documentos por día (50 × 100,000 = 5,000,000) desde Cloud Firestore.

Ahora supongamos que el sitio adopta la técnica anterior y almacena en caché esos 50 resultados durante un máximo de 5 minutos. Entonces, en lugar de cargar los resultados de la consulta para cada usuario, estos se cargan exactamente 12 veces por hora. Sin importar cuántos usuarios lleguen al sitio, la cantidad de consultas en Cloud Firestore se mantiene igual. De esta forma, en lugar de 5,000,000 de operaciones de lectura de documentos, en esta página se usarían 14,400 por día (12 × 24 × 50 = 14,400). Los pequeños costos adicionales de Firebase Hosting y Cloud Functions se compensan fácilmente con el ahorro de costos de Cloud Firestore.

Si bien el desarrollador se beneficia de este ahorro, quien obtiene más beneficios es el usuario. Si se cargan estos 50 documentos desde la CDN de Firebase Hosting en lugar de hacerlo directamente desde Cloud Firestore, se pueden ahorrar de 100 a 200 ms (o incluso más) de tiempo de carga del contenido en la página. Los estudios demuestran que las páginas que cargan rápido generan más satisfacción entre los usuarios.