Além dos SPAs: arquiteturas alternativas para seu PWA

Vamos falar sobre... arquitetura?

Vou abordar um tópico importante, mas que pode ser mal compreendido: a arquitetura que você usa para seu app da Web e, especificamente, como suas decisões de arquitetura entram em jogo ao criar um app da Web progressivo.

"Arquitetura" pode parecer um termo vago, e talvez não fique claro imediatamente por que isso é importante. Uma maneira de pensar sobre a arquitetura é se fazer as seguintes perguntas: quando um usuário acessa uma página no meu site, qual HTML é carregado? E o que é carregado quando eles visitam outra página?

As respostas a essas perguntas nem sempre são simples e, quando você começa a pensar em Apps Web Progressivos, elas podem ficar ainda mais complicadas. Por isso, meu objetivo é mostrar uma arquitetura possível que considero eficaz. Ao longo deste artigo, vou rotular as decisões que tomei como "minha abordagem" para criar um App Web Progressivo.

Você pode usar minha abordagem ao criar seu próprio PWA, mas, ao mesmo tempo, sempre há outras alternativas válidas. Espero que ver como todas as peças se encaixam sirva de inspiração e que você se sinta capaz de personalizar isso para atender às suas necessidades.

PWA do Stack Overflow

Para acompanhar este artigo, criei um PWA do Stack Overflow. Passo muito tempo lendo e contribuindo para o Stack Overflow, então queria criar um app da Web que facilitasse a navegação pelas perguntas frequentes sobre um determinado tópico. Ele é criado com base na API Stack Exchange pública. Ele é de código aberto, e você pode saber mais acessando o projeto do GitHub.

Apps de várias páginas (MPAs)

Antes de entrar em detalhes, vamos definir alguns termos e explicar partes da tecnologia subjacente. Primeiro, vou falar sobre o que gosto de chamar de "apps de várias páginas" ou "MPAs".

MPA é um nome sofisticado para a arquitetura tradicional usada desde o início da Web. Sempre que um usuário navega para um novo URL, o navegador renderiza progressivamente o HTML específico dessa página. Não há tentativa de preservar o estado da página ou o conteúdo entre as navegações. Cada vez que você visita uma nova página, começa do zero.

Isso é diferente do modelo de app de página única (SPA, na sigla em inglês) para criar apps da Web, em que o navegador executa o código JavaScript para atualizar a página atual quando o usuário acessa uma nova seção. Os dois são modelos igualmente válidos, mas, para esta postagem, eu queria explorar os conceitos de PWA no contexto de um app de várias páginas.

Rapidez confiável

Você já me ouviu (e inúmeras outras pessoas) usar a frase "progressive web app" ou PWA. Talvez você já conheça parte do material de referência, em outro lugar deste site.

Um PWA é um app da Web que oferece uma experiência do usuário de primeira classe e que realmente merece um lugar na tela inicial do usuário. A sigla "FIRE", que significa Fast (rápido), Integrated (integrado), Reliable (confiável) e Engaging (envolvente), resume todos os atributos que você precisa considerar ao criar um PWA.

Neste artigo, vou me concentrar em um subconjunto desses atributos: Rápido e Confiável.

Rápido: embora "rápido" signifique coisas diferentes em contextos diferentes, vou abordar os benefícios de velocidade de carregar o mínimo possível da rede.

Confiabilidade: mas a velocidade bruta não é suficiente. Para parecer um PWA, seu app da Web precisa ser confiável. Ele precisa ser resiliente o suficiente para sempre carregar algo, mesmo que seja apenas uma página de erro personalizada, independentemente do estado da rede.

Rapidez confiável: por fim, vou reformular um pouco a definição de PWA e analisar o que significa criar algo que seja rápido de forma confiável. Não basta ser rápido e confiável apenas quando você está em uma rede de baixa latência. Ser rápido de forma confiável significa que a velocidade do seu app da Web é consistente, independentemente das condições da rede.

Tecnologias de ativação: service workers + API Storage de Cache

Os PWAs introduzem um alto padrão de velocidade e resiliência. Felizmente, a plataforma da Web oferece alguns elementos básicos para tornar esse tipo de desempenho uma realidade. Estou me referindo aos service workers e à API Cache Storage.

É possível criar um service worker que detecta as solicitações recebidas, transmitindo algumas para a rede e armazenando uma cópia da resposta para uso futuro, usando a API Storage de Cache.

Um service worker usando a API Storage de Cache para salvar uma cópia de uma
          resposta de rede.

Na próxima vez que o app da Web fizer a mesma solicitação, o service worker poderá verificar os caches e retornar a resposta armazenada em cache anteriormente.

Um service worker que usa a API Cache Storage para responder, ignorando a rede.

Evitar a rede sempre que possível é uma parte crucial para oferecer um desempenho rápido e confiável.

JavaScript "isomórfico"

Outro conceito que quero abordar é o que às vezes é chamado de JavaScript "isomórfico" ou "universal". Em termos simples, é a ideia de que o mesmo código JavaScript pode ser compartilhado entre diferentes ambientes de execução. Quando criei meu PWA, queria compartilhar o código JavaScript entre o servidor de back-end e o service worker.

Há muitas abordagens válidas para compartilhar código dessa maneira, mas minha abordagem foi usar módulos ES como o código-fonte definitivo. Em seguida, transpilei e agrupei esses módulos para o servidor e o service worker usando uma combinação de Babel e Rollup. No meu projeto, os arquivos com uma extensão de arquivo .mjs são códigos que residem em um módulo ES.

O servidor

Com esses conceitos e terminologia em mente, vamos ver como eu realmente criei meu PWA do Stack Overflow. Vou começar abordando nosso servidor de back-end e explicar como ele se encaixa na arquitetura geral.

Eu estava procurando uma combinação de um back-end dinâmico com hospedagem estática, e minha abordagem foi usar a plataforma Firebase.

O Cloud Functions para Firebase ativa automaticamente um ambiente baseado em Node quando há uma solicitação recebida e se integra ao conhecido framework HTTP Express, com o qual eu já estava familiarizado. Ele também oferece hospedagem pronta para uso para todos os recursos estáticos do meu site. Vamos ver como o servidor lida com as solicitações.

Quando um navegador faz uma solicitação de navegação para nosso servidor, ele passa pelo seguinte fluxo:

Uma visão geral de como gerar uma resposta de navegação no lado do servidor.

O servidor encaminha a solicitação com base no URL e usa a lógica de modelagem para criar um documento HTML completo. Uso uma combinação de dados da API Stack Exchange e fragmentos HTML parciais que o servidor armazena localmente. Depois que o service worker sabe como responder, ele pode começar a transmitir HTML de volta para o app da Web.

Há duas partes dessa imagem que vale a pena analisar em mais detalhes: roteamento e modelagem.

Roteamento

Para o roteamento, minha abordagem foi usar a sintaxe de roteamento nativa do framework Express. Ele é flexível o suficiente para corresponder a prefixos de URL simples, bem como a URLs que incluem parâmetros como parte do caminho. Aqui, crio um mapeamento entre os nomes de rota e o padrão Express subjacente para correspondência.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

Em seguida, posso referenciar esse mapeamento diretamente no código do servidor. Quando há uma correspondência para um determinado padrão do Express, o gerenciador apropriado responde com uma lógica de modelo específica para a rota correspondente.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
  // Templating logic.
});

Modelos do lado do servidor

E como é essa lógica de modelagem? Bem, usei uma abordagem que juntava fragmentos HTML parciais em sequência, um após o outro. Esse modelo é ideal para streaming.

O servidor envia de volta um modelo HTML inicial imediatamente, e o navegador pode renderizar essa página parcial na hora. À medida que o servidor reúne o restante das fontes de dados, ele as transmite para o navegador até que o documento esteja completo.

Para entender o que quero dizer, confira o código do Express para uma das nossas rotas:

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Ao usar o método write() do objeto response e referenciar modelos parciais armazenados localmente, posso iniciar o stream de resposta imediatamente, sem bloquear nenhuma fonte de dados externa. O navegador usa esse HTML inicial e renderiza uma interface significativa e uma mensagem de carregamento imediatamente.

A próxima parte da nossa página usa dados da API Stack Exchange. Para receber esses dados, nosso servidor precisa fazer uma solicitação de rede. O app da Web não pode renderizar mais nada até receber uma resposta e processá-la, mas pelo menos os usuários não ficam olhando para uma tela em branco enquanto esperam.

Depois que o app da Web recebe a resposta da API Stack Exchange, ele chama uma função de modelo personalizada para traduzir os dados da API para o HTML correspondente.

Linguagem de modelagem

A criação de modelos pode ser um assunto surpreendentemente polêmico, e o que escolhi é apenas uma abordagem entre muitas. É recomendável substituir sua própria solução, especialmente se você tiver vínculos legados com um framework de modelos atual.

Para meu caso de uso, o ideal era usar os literais de modelo do JavaScript, com alguma lógica dividida em funções auxiliares. Uma das vantagens de criar um MPA é que você não precisa acompanhar as atualizações de estado e renderizar novamente o HTML. Por isso, uma abordagem básica que produziu HTML estático funcionou para mim.

Veja um exemplo de como estou criando um modelo para a parte HTML dinâmica do índice do meu app da Web. Assim como nas minhas rotas, a lógica de modelos é armazenada em um módulo ES que pode ser importado para o servidor e o service worker.

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

Essas funções de modelo são JavaScript puro, e é útil dividir a lógica em funções auxiliares menores quando apropriado. Aqui, transmito cada um dos itens retornados na resposta da API para uma dessas funções, que cria um elemento HTML padrão com todos os atributos apropriados definidos.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(id)}">${title}</a>`;
}

Vale destacar um atributo de dados que adiciono a cada link, data-cache-url, definido como o URL da API Stack Exchange que preciso para exibir a pergunta correspondente. Não se esqueça disso. Vou voltar a ele mais tarde.

Voltando ao gerenciador de rotas, quando o modelo estiver concluído, vou transmitir a parte final do HTML da página para o navegador e encerrar a transmissão. Essa é a indicação para o navegador de que a renderização progressiva foi concluída.

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Esse foi um tour rápido pela configuração do meu servidor. Os usuários que acessam meu app da Web pela primeira vez sempre recebem uma resposta do servidor, mas quando um visitante retorna ao meu app da Web, meu service worker começa a responder. Vamos analisar isso.

O service worker

Uma visão geral de como gerar uma resposta de navegação no service worker.

Este diagrama deve parecer familiar: muitos dos mesmos elementos que já abordei estão aqui em uma disposição um pouco diferente. Vamos analisar o fluxo de solicitação, considerando o service worker.

Nosso service worker processa uma solicitação de navegação recebida para um determinado URL e, assim como meu servidor, usa uma combinação de lógica de roteamento e modelagem para descobrir como responder.

A abordagem é a mesma de antes, mas com diferentes primitivas de baixo nível, como fetch() e a API Storage Cache. Uso essas fontes de dados para criar a resposta HTML, que o service worker transmite de volta ao app da Web.

Workbox

Em vez de começar do zero com primitivos de baixo nível, vou criar meu service worker com base em um conjunto de bibliotecas de alto nível chamado Workbox. Ele fornece uma base sólida para a lógica de armazenamento em cache, roteamento e geração de respostas de qualquer service worker.

Roteamento

Assim como no código do lado do servidor, o service worker precisa saber como associar uma solicitação recebida à lógica de resposta adequada.

Minha abordagem foi traduzir cada rota do Express em uma expressão regular correspondente, usando uma biblioteca útil chamada regexparam. Depois que essa tradução é realizada, posso aproveitar o suporte integrado do Workbox para roteamento de expressões regulares.

Depois de importar o módulo que tem as expressões regulares, registro cada expressão regular com o roteador do Workbox. Dentro de cada rota, posso fornecer uma lógica de modelo personalizada para gerar uma resposta. A criação de modelos no service worker é um pouco mais complexa do que no meu servidor de back-end, mas o Workbox ajuda com grande parte do trabalho pesado.

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

Armazenamento em cache de recursos estáticos

Uma parte importante da criação de modelos é garantir que meus modelos HTML parciais estejam disponíveis localmente pela API Cache Storage e sejam mantidos atualizados quando eu implantar mudanças no app da Web. A manutenção do cache pode ser propensa a erros quando feita manualmente, então uso o Workbox para lidar com o pré-armazenamento em cache como parte do meu processo de build.

Eu digo ao Workbox quais URLs devem ser pré-armazenados em cache usando um arquivo de configuração, apontando para o diretório que contém todos os meus recursos locais, além de um conjunto de padrões para correspondência. Esse arquivo é lido automaticamente pela CLI do Workbox, que é executada sempre que recrio o site.

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

O Workbox cria um snapshot do conteúdo de cada arquivo e injeta automaticamente essa lista de URLs e revisões no meu arquivo final do service worker. Agora o Workbox tem tudo o que precisa para disponibilizar os arquivos pré-armazenados em cache e mantê-los atualizados. O resultado é um arquivo service-worker.js que contém algo semelhante ao seguinte:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

Para quem usa um processo de build mais complexo, o Workbox tem um plug-in do webpack e um módulo de nó genérico, além da interface de linha de comando.

Streaming

Em seguida, quero que o service worker transmita esse HTML parcial pré-armazenado em cache de volta para o app da Web imediatamente. Essa é uma parte crucial de ser "confiavelmente rápido": sempre recebo algo significativo na tela imediatamente. Felizmente, o uso da API Streams no service worker torna isso possível.

Talvez você já tenha ouvido falar da API Streams. Meu colega Jake Archibald tem elogiado essa API há anos. Ele fez a previsão ousada de que 2016 seria o ano dos fluxos da web. A API Streams é tão incrível hoje quanto era há dois anos, mas com uma diferença crucial.

Na época, apenas o Chrome oferecia suporte a streams, mas agora a API Streams é amplamente compatível. A história geral é positiva e, com o código substituto apropriado, não há nada que impeça você de usar streams no service worker hoje mesmo.

Bem... talvez haja uma coisa que impeça você, e isso é entender como a API Streams realmente funciona. Ele expõe um conjunto muito eficiente de primitivos, e os desenvolvedores que se sentem confortáveis em usá-lo podem criar fluxos de dados complexos, como os seguintes:

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

No entanto, entender todas as implicações desse código pode não ser para todos. Em vez de analisar essa lógica, vamos falar sobre minha abordagem para o streaming de service workers.

Estou usando um wrapper de alto nível totalmente novo, workbox-streams. Com ele, posso transmitir uma combinação de fontes de streaming, tanto de caches quanto de dados de execução que podem vir da rede. O Workbox cuida da coordenação das fontes individuais e as une em uma única resposta de streaming.

Além disso, o Workbox detecta automaticamente se a API Streams é compatível e, quando não é, cria uma resposta equivalente que não é de streaming. Isso significa que você não precisa se preocupar em escrever substitutos, já que os streams estão cada vez mais próximos de 100% de suporte do navegador.

Armazenamento em cache no ambiente de execução

Vamos conferir como meu service worker lida com dados de ambiente de execução da API Stack Exchange. Estou usando o suporte integrado do Workbox para uma estratégia de armazenamento em cache stale-while-revalidate, além da expiração para garantir que o armazenamento do app da Web não cresça sem limites.

Configurei duas estratégias no Workbox para lidar com as diferentes fontes que vão compor a resposta de streaming. Com algumas chamadas de função e configurações, o Workbox nos permite fazer o que, de outra forma, levaria centenas de linhas de código escrito à mão.

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

A primeira estratégia lê dados que foram pré-armazenados em cache, como nossos modelos HTML parciais.

A outra estratégia implementa a lógica de armazenamento em cache stale-while-revalidate, além da expiração do cache menos usado recentemente quando atingimos 50 entradas.

Agora que essas estratégias estão em vigor, só falta informar ao Workbox como usá-las para criar uma resposta completa de streaming. Eu transmito uma matriz de origens como funções, e cada uma dessas funções será executada imediatamente. O Workbox pega o resultado de cada origem e o transmite para o app da Web, em sequência, atrasando apenas se a próxima função na matriz ainda não tiver sido concluída.

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'}),
  () => cacheStrategy.makeRequest({request: '/navbar.html'}),
  async ({event, url}) => {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, data.items);
  },
  () => cacheStrategy.makeRequest({request: '/foot.html'}),
]);

As duas primeiras fontes são modelos parciais pré-armazenados em cache lidos diretamente da API Storage, então sempre estarão disponíveis imediatamente. Isso garante que nossa implementação do service worker seja rápida e confiável ao responder a solicitações, assim como o código do lado do servidor.

Nossa próxima função de origem busca dados da API Stack Exchange e processa a resposta no HTML que o app da Web espera.

A estratégia stale-while-revalidate significa que, se eu tiver uma resposta armazenada em cache anteriormente para essa chamada de API, poderei transmiti-la para a página imediatamente, enquanto atualizo a entrada de cache "em segundo plano" para a próxima vez que ela for solicitada.

Por fim, transmito uma cópia em cache do rodapé e fecho as tags HTML finais para concluir a resposta.

O compartilhamento de código mantém tudo sincronizado

Você vai notar que algumas partes do código do service worker são familiares. A lógica de modelo e HTML parcial usada pelo service worker é idêntica à usada pelo gerenciador do lado do servidor. Esse compartilhamento de código garante que os usuários tenham uma experiência consistente, seja ao visitar meu app da Web pela primeira vez ou ao retornar a uma página renderizada pelo service worker. Essa é a beleza do JavaScript isomórfico.

Melhorias dinâmicas e progressivas

Já mostrei o servidor e o service worker do meu PWA, mas há uma última parte da lógica a ser abordada: há uma pequena quantidade de JavaScript que é executada em cada uma das minhas páginas, depois que elas são totalmente transmitidas.

Esse código melhora progressivamente a experiência do usuário, mas não é crucial. O app da Web ainda vai funcionar se não for executado.

Metadados da página

Meu app usa JavaScript do lado do cliente para atualizar os metadados de uma página com base na resposta da API. Como uso o mesmo bit inicial de HTML em cache para cada página, o app da Web acaba com tags genéricas no cabeçalho do documento. Mas, com a coordenação entre os modelos e o código do lado do cliente, posso atualizar o título da janela usando metadados específicos da página.

Como parte do código de modelo, minha abordagem é incluir uma tag script que contenha a string com escape correto.

const metadataScript = `<script>
  self._title = '${escape(item.title)}';
</script>`;

Depois, quando a página for carregada, vou ler essa string e atualizar o título do documento.

if (self._title) {
  document.title = unescape(self._title);
}

Se houver outros metadados específicos da página que você queira atualizar no seu web app, siga a mesma abordagem.

UX off-line

O outro aprimoramento progressivo que adicionei é usado para chamar a atenção para nossos recursos off-line. Criei um PWA confiável e quero que os usuários saibam que, quando estiverem off-line, ainda poderão carregar as páginas visitadas anteriormente.

Primeiro, uso a API Cache Storage para receber uma lista de todas as solicitações de API armazenadas em cache anteriormente e traduzo isso em uma lista de URLs.

Lembra daqueles atributos de dados especiais que mencionei, cada um contendo o URL da solicitação de API necessária para mostrar uma pergunta? Posso comparar esses atributos de dados com a lista de URLs em cache e criar uma matriz de todos os links de perguntas que não correspondem.

Quando o navegador entra em um estado off-line, percorro a lista de links não armazenados em cache e esmaece os que não funcionam. Essa é apenas uma dica visual para o usuário sobre o que esperar dessas páginas. Não estou desativando os links nem impedindo que o usuário navegue.

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

Armadilhas comuns

Agora, você já conhece minha abordagem para criar um PWA de várias páginas. Há muitos fatores que você precisa considerar ao criar sua própria abordagem, e talvez você faça escolhas diferentes das minhas. Essa flexibilidade é uma das grandes vantagens de criar para a Web.

Existem algumas armadilhas comuns que você pode encontrar ao tomar suas próprias decisões de arquitetura, e quero poupar você de algumas dores de cabeça.

Não armazene em cache o HTML completo

Não recomendo armazenar documentos HTML completos no cache. Primeiro, porque é um desperdício de espaço. Se o app da Web usar a mesma estrutura HTML básica para cada uma das páginas, você vai acabar armazenando cópias da mesma marcação repetidamente.

Além disso, se você implantar uma mudança na estrutura HTML compartilhada do site, todas as páginas armazenadas em cache anteriormente ainda vão ficar com o layout antigo. Imagine a frustração de um visitante recorrente ao ver uma mistura de páginas antigas e novas.

Desvio do servidor / service worker

Outro problema a ser evitado envolve o servidor e o service worker ficarem fora de sincronia. Minha abordagem foi usar o JavaScript isomórfico para que o mesmo código fosse executado nos dois lugares. Dependendo da arquitetura do servidor, isso nem sempre é possível.

Seja qual for a decisão de arquitetura que você tomar, é preciso ter alguma estratégia para executar o código equivalente de roteamento e modelagem no servidor e no service worker.

Piores cenários

Layout / design inconsistente

O que acontece quando você ignora essas armadilhas? Bem, todos os tipos de falhas são possíveis, mas o pior cenário é que um usuário recorrente visite uma página em cache com um layout muito desatualizado, talvez com um texto de cabeçalho desatualizado ou que use nomes de classe CSS que não são mais válidos.

Pior cenário: roteamento corrompido

Como alternativa, um usuário pode encontrar um URL que é processado pelo seu servidor, mas não pelo service worker. Um site cheio de layouts zumbis e becos sem saída não é um PWA confiável.

Dicas de sucesso

Mas você não está só! As dicas a seguir podem ajudar você a evitar essas armadilhas:

Use bibliotecas de modelos e roteamento que tenham implementações em vários idiomas

Tente usar bibliotecas de modelos e roteamento que tenham implementações em JavaScript. Eu sei que nem todo desenvolvedor tem o luxo de migrar do servidor da Web e da linguagem de modelo atuais.

No entanto, vários frameworks conhecidos de modelagem e roteamento têm implementações em várias linguagens. Se você encontrar um que funcione com JavaScript e com a linguagem do seu servidor atual, estará um passo mais perto de manter o service worker e o servidor sincronizados.

Preferir modelos sequenciais em vez de aninhados

Em seguida, recomendo usar uma série de modelos sequenciais que podem ser transmitidos um atrás do outro. Não há problema se as partes posteriores da página usarem uma lógica de modelo mais complicada, desde que você possa transmitir a parte inicial do HTML o mais rápido possível.

Armazenar em cache conteúdo estático e dinâmico no service worker

Para ter o melhor desempenho, faça o pré-armazenamento em cache de todos os recursos estáticos essenciais do site. Também é necessário configurar a lógica de armazenamento em cache do ambiente de execução para processar conteúdo dinâmico, como solicitações de API. Usar o Workbox significa que você pode criar com base em estratégias bem testadas e prontas para produção, em vez de implementar tudo do zero.

Bloqueie na rede apenas quando for absolutamente necessário

Além disso, você só deve bloquear a rede quando não for possível fazer streaming de uma resposta do cache. Mostrar uma resposta da API armazenada em cache imediatamente pode melhorar a experiência do usuário em comparação com esperar por dados novos.

Recursos