Além dos SPAs: arquiteturas alternativas para seu PWA

Vamos falar sobre... arquitetura?

Abordarei um tópico importante, mas possivelmente mal interpretado: a arquitetura que você usa para seu app da Web e, especificamente, como suas decisões de arquitetura entram em jogo quando você está criando um Progressive Web App.

"Arquitetura" pode parecer vaga, e pode não ficar imediatamente claro por que isso é importante. Uma maneira de pensar sobre arquitetura é fazer a si mesmo as seguintes perguntas: quando um usuário visita uma página do meu site, qual HTML é carregado? E o que é carregado quando eles acessam outra página?

As respostas a essas perguntas nem sempre são diretas e, quando você começar a pensar em Progressive Web Apps, eles podem ficar ainda mais complicados. Portanto, meu objetivo é mostrar para você uma possível arquitetura eficaz. Ao longo deste artigo, vou rotular as decisões que tomei como "minha abordagem" para a criação de um Progressive Web App.

Você pode usar minha abordagem ao criar seu próprio PWA, mas, ao mesmo tempo, há sempre outras alternativas válidas. Minha esperança é que ver como todas as peças se encaixam inspire você e que se sinta capacitado para personalizar isso para atender às suas necessidades.

PWA do Stack Overflow

Para acompanhar este artigo, criei um PWA do Stack Overflow (em inglês). Passo muito tempo lendo e contribuindo para o Stack Overflow (link em inglês) e eu queria criar um app da Web que facilitasse a navegação nas perguntas frequentes sobre um determinado tópico. Ele é baseado na API Stack Exchange pública. É de código aberto, e você pode saber mais visitando o projeto do GitHub (link em inglês).

Apps de várias páginas (MPAs)

Antes de mostrar os detalhes, vamos definir alguns termos e explicar as partes da tecnologia. Primeiro, abordarei 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. Cada vez que um usuário acessa 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, você está começando 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 existente quando o usuário visita uma nova seção. SPAs e MPAs são modelos igualmente válidos, mas, nesta postagem, quero explorar os conceitos de PWA no contexto de um app de várias páginas.

Confiávelmente rápido

Você já ouviu eu (e inúmeros outros) usar a frase "Progressive Web App", ou PWA. Talvez você já conheça alguns dos materiais de plano de fundo em outro lugar deste site.

Pense no PWA como um app da Web que oferece uma experiência do usuário de primeira classe e que realmente ocupa um lugar na tela inicial do usuário. O acrônimo "FIRE", que corresponde a Fast, Integated, Reliable e Engaging, resume todos os atributos que precisam ser considerados 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 distintos, vamos abordar os benefícios de velocidade do carregamento o mínimo possível da rede.

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

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

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

Os PWAs apresentam 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 detecte solicitações recebidas, transmitindo algumas para a rede e armazenando uma cópia da resposta para uso futuro por meio da API Cache Storage.

Um service worker usando a API Cache Storage 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 anterior.

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

Para oferecer um desempenho confiável e rápido, é essencial evitar a rede sempre que possível.

JavaScript "isomórfico"

Outro conceito que quero abordar é o que às vezes é chamado de JavaScript "isomórfico" ou "universal". Simplificando, é a ideia de que o mesmo código JavaScript pode ser compartilhado entre diferentes ambientes de execução. Quando criei meu PWA, queria compartilhar um código JavaScript entre meu 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 a extensão .mjs são códigos que residem em um módulo ES.

O servidor

Com esses conceitos e terminologia em mente, vamos nos aprofundar em como criei meu PWA do Stack Overflow. Começarei abordando nosso servidor de back-end e explicarei como isso se encaixa na arquitetura geral.

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

O Firebase Cloud Functions ativa automaticamente um ambiente baseado em nó quando há uma solicitação recebida e se integra ao conhecido framework Express HTTP, que eu já conhecia. Ele também oferece hospedagem pronta para uso para todos os recursos estáticos do meu site. Vamos dar uma conferida em como o servidor processa as solicitações.

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

Visão geral da geração de 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 modelos para criar um documento HTML completo. Uso uma combinação de dados da API Stack Exchange, bem como fragmentos HTML parciais que o servidor armazena localmente. Assim que o service worker souber como responder, ele poderá começar a transmitir HTML de volta para nosso app da Web.

Vale a pena explorar mais duas partes dessa imagem: o roteamento e os modelos.

Roteamento

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

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

export default routes;

Posso então fazer referência a esse mapeamento diretamente no código do servidor. Quando há uma correspondência para um determinado padrão 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 modelo? Bem, eu adotei uma abordagem que reuniu fragmentos HTML parciais em sequência, um após o outro. Esse modelo é adequado para streaming.

O servidor envia de volta um código padrão HTML inicial imediatamente, e o navegador pode renderizar essa página parcial imediatamente. À medida que o servidor reúne o restante das fontes de dados, ele as transmite para o navegador até que o documento seja concluído.

Para entender o que quero dizer, consulte o Código expresso de 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 fazer referência a modelos parciais armazenados localmente, consigo 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 carrega a mensagem imediatamente.

A próxima parte da nossa página usa dados da API Stack Exchange. Conseguir esses dados significa que o servidor precisa fazer uma solicitação de rede. O app da Web não pode renderizar nada mais até que receba uma resposta e faça o processamento, mas pelo menos os usuários não estão encarando uma tela em branco enquanto aguardam.

Depois que o app da Web recebe a resposta da API Stack Exchange, ele chama uma função de modelos personalizados para converter os dados da API no HTML correspondente.

Linguagem de modelos

Modelos podem ser um tópico surpreendentemente controverso, e o que eu selecionei é apenas uma entre muitas abordagens. Substitua sua própria solução, especialmente se você tiver vínculos legados com um framework de modelos atual.

O que fazia sentido para meu caso de uso foi confiar apenas nos literais de modelo do JavaScript, com alguma lógica dividida em funções auxiliares. Uma das vantagens de criar uma MPA é que você não precisa acompanhar as atualizações de estado e renderizar novamente o HTML. Portanto, uma abordagem básica que produziu o HTML estático funcionou para mim.

Este é um exemplo de como criar modelos para a parte de HTML dinâmico 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, transfiro 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>`;
}

Particularmente é um atributo de dados que eu adiciono a cada link, data-cache-url, definido como o URL da API Stack Exchange necessário para exibir a pergunta correspondente. Tenha isso em mente. Vou rever mais tarde.

Voltando ao gerenciador de rotas, assim que os modelos estiverem concluídos, eu transfiro a parte final do HTML da minha página para o navegador e finalizo o fluxo. Essa é a dica 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();
});

Este é um tour rápido da configuração do meu servidor. Os usuários que acessarem meu app da Web pela primeira vez sempre receberão uma resposta do servidor, mas quando um visitante retornar ao meu app da Web, o service worker começará a responder. Vamos nos aprofundar nisso.

O service worker

Uma visão geral da geração de uma resposta de navegação no service worker.

Esse diagrama deve parecer familiar. Muitas das mesmas partes que abordamos anteriormente estão aqui com uma organização um pouco diferente. Vamos examinar o fluxo de solicitação e considerar o service worker.

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

A abordagem é a mesma de antes, mas com diferentes primitivos de baixo nível, como fetch() e a API Cache Storage. Elas são usadas para construir 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, criarei meu service worker sobre um conjunto de bibliotecas de alto nível chamado Workbox. Ele fornece uma base sólida para a lógica de geração de respostas, roteamento e armazenamento em cache de qualquer service worker.

Roteamento

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

Minha abordagem foi converter cada rota do Express em uma expressão regular correspondente, usando uma biblioteca útil chamada regexparam. Depois que a conversã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 lógica de modelos personalizada para gerar uma resposta. A criação de modelos no service worker é um pouco mais complexa do que no servidor de back-end, mas o Workbox ajuda muito com o trabalho pesado.

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

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

Armazenamento em cache de ativos estáticos

Uma parte fundamental da história dos modelos é garantir que meus modelos HTML parciais estejam disponíveis localmente por meio da API Cache Storage e sejam atualizados quando eu implantar alterações no app da Web. A manutenção do cache pode ser propensa a erros quando feita manualmente, então recorro ao Workbox para lidar com o armazenamento em cache como parte do meu processo de criação.

Eu insiro 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 e um conjunto de padrões correspondentes. Esse arquivo é lido automaticamente pela CLI do Workbox, que é run sempre que o site é recriado.

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

O Workbox faz um snapshot do conteúdo de cada arquivo e injeta automaticamente essa lista de URLs e revisões no meu arquivo de service worker final. O Workbox agora tem tudo o que é necessário para manter os arquivos pré-armazenados em cache sempre disponíveis e 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 pessoas que usam um processo de build mais complexo, o Workbox tem um plug-in webpack e um módulo de nó genérico, além da interface de linha de comando (links em inglês).

Streaming

Em seguida, quero que o service worker faça streaming desse HTML parcial pré-armazenado em cache de volta para o app da Web imediatamente. Essa é uma parte crucial para ser "confiávelmente rápido". Sempre vejo algo significativo na tela imediatamente. Felizmente, o uso da API Streams no nosso service worker torna isso possível.

Talvez você já tenha ouvido falar da API Streams. Meu colega Jake Archibald canta seus elogios por anos. Ele fez a previsão arrojada de que 2016 seria o ano dos fluxos da Web. A API Streams é tão incrível hoje quanto era dois anos atrás, mas com uma diferença crucial.

Naquela época, apenas o Chrome era compatível com o Streams, mas agora a API Streams tem um suporte mais amplo. A história geral é positiva e, com o código substituto adequado, você não pode impedir seu uso atual de streams no service worker.

Bem, pode haver algo impedindo você e que esteja fazendo você saber como a API Streams realmente funciona. Ele expõe um conjunto muito poderoso de primitivos, e os desenvolvedores que se sentem à vontade para 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 as implicações completas desse código pode não ser para todos. Em vez de analisar por essa lógica, vamos falar sobre minha abordagem de streaming de service worker.

Estou usando um novo wrapper de alto nível, workbox-streams. Com ela, é possível transmiti-la em uma combinação de origens de streaming, tanto de caches quanto de dados de execução que podem vir da rede. O Workbox coordena as origens individuais e as junta em uma única resposta de streaming.

Além disso, o Workbox detecta automaticamente se a API Streams tem suporte e, quando não é, cria uma resposta equivalente que não seja de streaming. Isso significa que você não precisa se preocupar em criar substitutos, já que os streams estão mais perto de oferecer suporte total aos navegadores.

Armazenamento em cache do ambiente de execução

Vamos ver como meu service worker lida com os dados do ambiente de execução usando a API Stack Exchange. Estou usando o suporte integrado do Workbox a uma estratégia de armazenamento em cache obsoleta durante a revalidação, junto à expiração para garantir que o armazenamento do app da Web não aumente ilimitado.

Configurei duas estratégias no Workbox para lidar com as diferentes fontes que vão compor a resposta de streaming. Em algumas chamadas de função e configuração, o Workbox permite fazer o que, de outra forma, seria necessário centenas de linhas de código escrito.

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ê os dados que foram pré-armazenados em cache, como nossos modelos HTML parciais.

A outra estratégia implementa a lógica de cache obsoleto durante a revalidação, junto com a expiração do cache menos usada recentemente quando atingirmos 50 entradas.

Agora que essas estratégias foram implementadas, só falta informar ao Workbox como usá-las para criar uma resposta de streaming completa. Eu passo uma matriz de origens como funções, e cada uma dessas funções será executada imediatamente. O Workbox recebe o resultado de cada origem e faz o streaming dele 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 Cache Storage. Portanto, elas estarão sempre disponíveis imediatamente. Isso garante que nossa implementação do service worker seja rápida e confiável na resposta às solicitações, assim como meu código do lado do servidor.

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

A estratégia de inatividade durante a revalidação significa que, se eu tiver uma resposta armazenada em cache anteriormente para esta chamada de API, vou conseguir fazer streaming dela para a página imediatamente, enquanto atualiza a entrada de cache "em segundo plano" para a próxima vez que for solicitada.

Por fim, transfiro 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 em sincronia

Você vai notar que alguns elementos do código do service worker parecem familiares. A lógica de HTML e de modelos 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, esteja eles acessando meu app da Web pela primeira vez ou retornando a uma página renderizada pelo service worker. Essa é a beleza do JavaScript isomórfico.

Melhorias dinâmicas e progressivas

Analisei o servidor e o service worker do meu PWA, mas há uma última lógica a ser abordada: uma pequena quantidade de JavaScript é executada em cada uma das minhas páginas depois do streaming completo.

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

Metadados da página

Meu app usa o JavaScript do lado do cliente para atualizar os metadados de uma página com base na resposta da API. Como eu uso o mesmo pedaço inicial de HTML em cache para cada página, o app da Web acaba com tags genéricas no cabeçalho do meu documento. No entanto, por meio da 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 de script que contenha a string com escape correto.

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

Depois, quando minha página for carregada, li essa string e atualizo 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 próprio app da Web, siga a mesma abordagem.

UX off-line

As outras melhorias progressivas que adicionei é usada 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 páginas visitadas anteriormente.

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

Você se lembra dos atributos de dados especiais que mencionamos, cada um contendo o URL da solicitação de API necessária para exibir uma pergunta? Posso cruzar esses atributos de dados com a lista de URLs armazenados 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, eu percorra a lista de links sem cache e esmaece aqueles que não vão funcionar. Lembre-se de que essa é apenas uma dica visual para o usuário sobre o que ele deve esperar dessas páginas. Na verdade, não estou desativando os links nem impedindo o usuário de navegar.

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

Dificuldades comuns

Fiz um tour pela minha abordagem para criar um PWA de várias páginas. Há muitos fatores que você terá que considerar ao elaborar sua própria abordagem, e você pode acabar fazendo escolhas diferentes das que fiz. Essa flexibilidade é uma das melhores coisas de criar para a Web.

Há algumas armadilhas comuns que você pode encontrar ao tomar suas próprias decisões de arquitetura, e quero evitar um pouco de dor.

Não armazenar HTML completo em cache

Não recomendamos armazenar documentos HTML completos em seu cache. Para começar, é um desperdício de espaço. Se o app da Web usar a mesma estrutura HTML básica para cada uma das páginas, as cópias da mesma marcação serão armazenadas novamente.

E o mais importante é que, se você implantar uma mudança na estrutura HTML compartilhada do site, todas as páginas previamente armazenadas em cache vão continuar presas no layout antigo. Imagine a frustração de um visitante recorrente ao ver uma combinação de páginas antigas e novas.

Desvio do servidor / service worker

A outra armadilha a ser evitada envolve o servidor e o service worker dessincronizados. Minha abordagem era usar JavaScript isomórfico, para que o mesmo código fosse executado nos dois locais. Dependendo da arquitetura do servidor atual, isso nem sempre é possível.

Independentemente das decisões arquitetônicas que você tomar, é necessário ter alguma estratégia para executar o código de roteamento e modelo equivalente no servidor e no service worker.

Pior cenários

Layout / design inconsistente

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

Pior cenário: trajeto corrompido

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

Dicas para alcançar o sucesso

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

Usar bibliotecas de modelos e roteamento com implementações em várias linguagens

Tente usar bibliotecas de modelos e roteamento com implementações de JavaScript. Nem todo desenvolvedor pode se dar ao luxo de migrar do seu servidor da Web atual e criar modelos de linguagem.

No entanto, vários frameworks de modelos e roteamento têm implementações em várias linguagens. Se você encontrar um que funcione com JavaScript e também com a linguagem do seu servidor atual, estará um passo mais perto de manter seu 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 após o outro. Não há problema se partes posteriores da sua página usarem uma lógica de modelos mais complicada, desde que você possa fazer streaming na parte inicial do HTML o mais rápido possível.

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

Para melhorar o desempenho, armazene todos os recursos estáticos críticos do site em cache. Também é necessário configurar a lógica de armazenamento em cache no 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.

Só use bloqueios na rede quando for absolutamente necessário

Em relação a isso, só bloqueie na rede quando não for possível transmitir uma resposta do cache. A exibição imediata de uma resposta de API armazenada em cache pode resultar em uma experiência do usuário melhor do que esperar por novos dados.

Recursos