Servir contenido de Firestore incluido desde una CDN

Muchas aplicaciones ofrecen el mismo contenido a todos los usuarios al cargar la primera página. Por ejemplo, un sitio de noticias puede mostrar las últimas noticias o un sitio de comercio electrónico puede mostrar los artículos más vendidos.

Si este contenido se proporciona desde Cloud Firestore, cada usuario emitirá una nueva consulta para obtener los mismos resultados cuando carguen la aplicación. Debido a que estos resultados no se almacenan en caché entre los usuarios, la aplicación es más lenta y más cara de lo necesario.

Solución: paquetes

Los paquetes de Cloud Firestore te permiten ensamblar paquetes de datos a partir de resultados de consultas comunes en el backend usando el SDK de Firebase Admin y servir estos blobs precalculados almacenados en caché en una CDN. Esto brinda a sus usuarios una experiencia de primera carga mucho más rápida y reduce los costos de consulta de Cloud Firestore.

En esta guía usaremos Cloud Functions para generar paquetes y Firebase Hosting para almacenar en caché y servir contenido de paquetes de forma dinámica. Más información sobre paquetes está disponible en las guías .

Primero cree una función HTTP pública simple para consultar las 50 "historias" más recientes y ofrecer el resultado como un paquete.

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

A continuación, configure Firebase Hosting para servir y almacenar en caché esta función de nube modificando firebase.json . Con esta configuración, la CDN de Firebase Hosting ofrecerá el contenido del paquete de acuerdo con la configuración de caché establecida por la función de nube. Cuando el caché caduque, actualizará el contenido activando la función nuevamente.

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

Finalmente, en su aplicación web, obtenga el contenido incluido de la CDN y cárguelo 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
  // ...
}

Ahorros estimados

Considere un sitio web de noticias que recibe 100.000 usuarios por día y cada usuario carga las mismas 50 noticias importantes en la carga inicial. Sin ningún almacenamiento en caché, esto daría como resultado 50 x 100 000 = 5 000 000 de lecturas de documentos por día 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, los resultados se cargan exactamente 12 veces por hora. No importa cuántos usuarios lleguen al sitio, la cantidad de consultas a Cloud Firestore sigue siendo la misma. En lugar de 5.000.000 de lecturas de documentos, esta página utilizaría 12 x 24 x 50 = 14.400 lecturas de documentos por día. Los pequeños costos adicionales de Firebase Hosting y Cloud Functions se compensan fácilmente con los ahorros de costos de Cloud Firestore.

Si bien el desarrollador se beneficia del ahorro de costos, el mayor beneficiario es el usuario. Cargar estos 50 documentos desde la CDN de Firebase Hosting en lugar de directamente desde Cloud Firestore puede reducir fácilmente entre 100 y 200 ms o más el tiempo de carga del contenido de la página. Los estudios han demostrado repetidamente que las páginas rápidas significan usuarios más felices.