Exibir conteúdo empacotado do Firestore usando uma CDN

Muitos aplicativos exibem o mesmo conteúdo para todos os usuários no primeiro carregamento de página. Por exemplo, um site de notícias pode mostrar as histórias mais recentes, ou um site de e-commerce pode mostrar os itens mais vendidos.

Se esse conteúdo for exibido usando o Cloud Firestore, cada usuário emitirá uma nova consulta para os mesmos resultados quando ele carregar o aplicativo. Como esses resultados não são armazenados em cache entre os usuários, o aplicativo é mais lento e caro do que deveria ser.

Solução: pacotes

O Cloud Firestore permite montar pacotes de dados a partir de resultados de consulta comuns no back-end usando o SDK Admin do Firebase. Ele também permite disponibilizar esses blobs pré-computados em cache em uma CDN. Isso oferece aos usuários uma experiência de primeiro carregamento muito mais rápida e reduz os custos de consulta do Cloud Firestore.

Neste guia, usaremos o Cloud Functions para gerar pacotes e o Firebase Hosting para armazenar em cache e exibir conteúdo do pacote dinamicamente. Para mais informações sobre pacotes, consulte os guias.

Primeiro, crie uma função HTTP pública simples para consultar as 50 "histórias" mais recentes e exibir o resultado como um pacote.

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

Em seguida, modifique firebase.json para configurar o Firebase Hosting de modo a exibir e armazenar em cache essa função do Cloud. Com essa configuração, a CDN do Firebase Hosting exibirá o conteúdo do pacote de acordo com as configurações de cache definidas pela função do Cloud. Quando o cache expirar, ele ativará a função novamente para atualizar o conteúdo.

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

Por fim, no seu aplicativo da Web, busque o conteúdo empacotado da CDN e carregue-o no SDK do 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
  // ...
}

Economia estimada

Imagine um site de notícias que recebe 100.000 usuários por dia, e cada usuário carrega as mesmas 50 histórias principais no carregamento inicial. Sem nenhum armazenamento em cache, isso resultaria em 50 x 100.000 = 5.000.000 leituras de documentos por dia do Cloud Firestore.

Agora, suponha que o site adote a técnica acima e armazene esses 50 resultados em cache por até cinco minutos. Nesse caso, em vez de carregar os resultados da consulta para cada usuário, eles serão carregados exatamente 12 vezes por hora. Não importa quantos usuários acessem ao site, o número de consultas ao Cloud Firestore permanecerá o mesmo. Em vez de 5.000.000 leituras de documentos, essa página usaria 12 x 24 x 50 = 14.400 leituras de documentos por dia. Os pequenos custos extras do Firebase Hosting e do Cloud Functions são facilmente compensados pela economia de custos do Cloud Firestore.

Embora o desenvolvedor se beneficie da economia de custos, quem ganha mais no final é o usuário. O carregamento desses 50 documentos da CDN do Firebase Hosting em vez de usar o Cloud Firestore diretamente pode remover de 100 a 200 ms ou mais do tempo de carregamento do conteúdo da página. Estudos mostram repetidamente que páginas rápidas significam usuários mais felizes.