Carregamento instantâneo de apps da Web com uma arquitetura de shell de aplicativo

Um shell do aplicativo é o mínimo de HTML, CSS e JavaScript que alimentam uma interface do usuário. O shell do aplicativo precisa:

  • carregamento rápido
  • ser armazenada em cache
  • mostrar conteúdo dinamicamente

Um shell de aplicativo é o segredo para um bom desempenho confiável. Pense no shell do app como o pacote de código que você publicaria em uma app store se estivesse criando um app nativo. É a carga necessária para começar, mas talvez não seja a história toda. Ele mantém a interface local e extrai conteúdo dinamicamente por uma API.

Separação de shell de app de HTML, JS e CSS e o conteúdo HTML

Contexto

O artigo Apps da Web Progressivos de Alex Russell descreve como um app da Web pode mudar progressivamente com o uso e o consentimento do usuário para oferecer uma experiência mais semelhante a um app nativo, com suporte off-line, notificações push e a capacidade de ser adicionado à tela inicial. Isso depende muito dos benefícios de funcionalidade e desempenho do service worker e das capacidades de armazenamento em cache. Isso permite que você se concentre na velocidade, oferecendo aos seus apps da Web o mesmo carregamento instantâneo e as atualizações regulares que você está acostumado a ver em aplicativos nativos.

Para aproveitar ao máximo esses recursos, precisamos de uma nova maneira de pensar sobre os sites: a arquitetura de shell do aplicativo.

Vamos entender como estruturar seu app usando uma arquitetura de shell de aplicativo aumentada de service worker. Vamos analisar a renderização do cliente e do servidor e compartilhar um exemplo completo que você pode testar hoje mesmo.

Para enfatizar o ponto, o exemplo abaixo mostra o primeiro carregamento de um app usando essa arquitetura. Observe a mensagem "O app está pronto para uso off-line" na parte de baixo da tela. Se uma atualização do shell ficar disponível mais tarde, podemos informar ao usuário para atualizar a nova versão.

Imagem de um worker de serviço em execução no DevTools para o shell do aplicativo

O que são service workers?

Um service worker é um script executado em segundo plano, separado da sua página da Web. Ele responde a eventos, incluindo solicitações de rede feitas pelas páginas que ele serve e notificações push do servidor. Um worker de serviço tem uma vida útil intencionalmente curta. Ela é ativada quando recebe um evento e é executada apenas pelo tempo necessário para processá-lo.

Os workers de serviço também têm um conjunto limitado de APIs em comparação com o JavaScript em um contexto de navegação normal. Isso é padrão para workers na Web. Um worker de serviço não pode acessar o DOM, mas pode acessar coisas como a API Cache e fazer solicitações de rede usando a API Fetch. A API IndexedDB e o postMessage() também estão disponíveis para uso na persistência de dados e na troca de mensagens entre o service worker e as páginas controladas por ele. Os eventos push enviados pelo servidor podem invocar a API Notification para aumentar o engajamento do usuário.

Um worker de serviço pode interceptar solicitações de rede feitas em uma página (que aciona um evento de busca no worker de serviço) e retornar uma resposta recuperada da rede ou de um cache local, ou até mesmo construída de forma programática. Na prática, é um proxy programável no navegador. O interessante é que, independentemente de onde a resposta vem, ela parece para a página da Web como se não houvesse envolvimento do service worker.

Para saber mais sobre os service workers, leia Introdução aos service workers.

Benefícios de desempenho

Os Service Workers são poderosos para o armazenamento em cache off-line, mas também oferecem ganhos significativos de desempenho na forma de carregamento instantâneo para visitas repetidas ao seu site ou app da Web. É possível armazenar em cache o shell do aplicativo para que ele funcione off-line e preencher o conteúdo usando JavaScript.

Nas visitas repetidas, isso permite que você tenha pixels significativos na tela sem a rede, mesmo que seu conteúdo venha dela. Pense nisso como mostrar barras de ferramentas e cards imediatamente e carregar o restante do conteúdo gradualmente.

Para testar essa arquitetura em dispositivos reais, executamos nosso exemplo de shell do aplicativo no WebPageTest.org e mostramos os resultados abaixo.

Teste 1:teste com cabo em um Nexus 5 usando o Chrome Dev

A primeira visualização do app precisa buscar todos os recursos da rede e não consegue uma pintura significativa até 1,2 segundo. Graças ao armazenamento em cache do service worker, nossa visita repetida alcança uma pintura significativa e termina o carregamento em 0,5 segundo.

Diagrama de pintura de teste da página da Web para conexão por cabo

Teste 2:teste em 3G com um Nexus 5 usando o Chrome Dev

Também podemos testar nosso exemplo com uma conexão 3G um pouco mais lenta. Dessa vez, a primeira pintura significativa leva 2,5 segundos na primeira visita. Leva 7,1 segundos para carregar a página completamente. Com o armazenamento em cache do worker de serviço, nossa visita repetida alcança uma pintura significativa e termina o carregamento em 0,8 segundos.

Diagrama de pintura de teste de página da Web para conexão 3G

Outras visualizações contam uma história semelhante. Compare os 3 segundos necessários para alcançar a primeira pintura significativa no shell do aplicativo:

Cronograma de pintura para a primeira visualização do teste de página da Web

para os 0,9 segundos necessários quando a mesma página é carregada do cache do service worker. Mais de dois segundos são economizados para nossos usuários finais.

Cronograma de pintura para visualização repetida do teste de página da Web

É possível ter ganhos de desempenho semelhantes e confiáveis para seus próprios aplicativos usando a arquitetura de shell do aplicativo.

O worker de serviço exige que repensemos a estrutura dos apps?

Os service workers implicam algumas mudanças sutis na arquitetura do aplicativo. Em vez de agrupar todo o aplicativo em uma string HTML, pode ser útil fazer coisas no estilo AJAX. É onde você tem um shell (que está sempre em cache e pode ser inicializado sem a rede) e conteúdo atualizado regularmente e gerenciado separadamente.

As implicações dessa divisão são grandes. Na primeira visita, você pode renderizar conteúdo no servidor e instalar o service worker no cliente. Nas visitas seguintes, você só precisa solicitar dados.

E o aprimoramento progressivo?

Embora o service worker não tenha suporte de todos os navegadores, a arquitetura do shell de conteúdo do aplicativo usa o aprimoramento progressivo para garantir que todos possam acessar o conteúdo. Por exemplo, nosso projeto de exemplo.

Abaixo, você pode conferir a versão completa renderizada no Chrome, Firefox Nightly e Safari. À esquerda, você pode ver a versão do Safari em que o conteúdo é renderizado no servidor sem um worker de serviço. À direita, vemos as versões do Chrome e do Firefox Nightly com suporte do worker de serviço.

Imagem do shell do aplicativo carregado no Safari, Chrome e Firefox

Quando faz sentido usar essa arquitetura?

A arquitetura de shell do aplicativo é mais adequada para apps e sites dinâmicos. Se o site for pequeno e estático, provavelmente você não vai precisar de um shell de aplicativo e poderá armazenar em cache todo o site em uma etapa oninstall do service worker. Use a abordagem que fizer mais sentido para seu projeto. Várias estruturas JavaScript já incentivam a divisão da lógica do aplicativo do conteúdo, o que torna esse padrão mais simples de aplicar.

Já existem apps de produção que usam esse padrão?

A arquitetura de shell do aplicativo é possível com apenas algumas mudanças na interface geral do aplicativo e funcionou bem para sites de grande escala, como o app da Web progressiva da I/O 2015 e a caixa de entrada do Google.

Imagem da Google Inbox carregando. Ilustra a caixa de entrada usando o service worker.

Os shells de aplicativos off-line são uma grande vantagem de desempenho e também são bem demonstrados no app off-line da Wikipedia de Jake Archibald e no app da Web progressivo Flipkart Lite.

Capturas de tela da demonstração da Wikipedia de Jake Archibald.

Explicação da arquitetura

Durante a primeira experiência de carregamento, seu objetivo é exibir conteúdo significativo na tela do usuário o mais rápido possível.

Primeiro carregamento e carregamento de outras páginas

Diagrama da primeira carga com o shell do app

Em geral, a arquitetura do shell do aplicativo:

  • Priorize o carregamento inicial, mas deixe o service worker armazenar em cache o shell do aplicativo para que as visitas repetidas não exijam que o shell seja buscado novamente da rede.

  • Carregue tudo em segundo plano ou com carregamento lento. Uma boa opção é usar o armazenamento em cache de leitura para conteúdo dinâmico.

  • Use ferramentas de worker de serviço, como sw-precache, para armazenar em cache e atualizar de forma confiável o worker de serviço que gerencia o conteúdo estático. Vamos falar mais sobre o sw-precache mais tarde.

Para fazer isso:

  • O servidor vai enviar conteúdo HTML que o cliente pode renderizar e usar cabeçalhos de expiração de cache HTTP de futuro distante para considerar navegadores sem suporte a worker de serviço. Ele vai exibir nomes de arquivos usando hashes para permitir a "versão" e atualizações fáceis para mais tarde no ciclo de vida do aplicativo.

  • Página(s) vai incluir estilos CSS inline em uma tag <style> no documento <head> para fornecer uma primeira pintura rápida do shell do aplicativo. Cada página vai carregar de forma assíncrona o JavaScript necessário para a visualização atual. Como o CSS não pode ser carregado de forma assíncrona, podemos solicitar estilos usando JavaScript, que é assíncrono, em vez de ser orientado por parser e síncrono. Também podemos aproveitar o requestAnimationFrame() para evitar casos em que podemos ter uma inserção rápida no cache e os estilos se tornarem acidentalmente parte do caminho de renderização crítico. requestAnimationFrame() força a pintura do primeiro frame antes do carregamento dos estilos. Outra opção é usar projetos como o loadCSS do Filament Group para solicitar CSS de forma assíncrona usando JavaScript.

  • O service worker armazena uma entrada em cache do shell do aplicativo para que, nas visitas repetidas, o shell possa ser carregado inteiramente do cache do service worker, a menos que uma atualização esteja disponível na rede.

Shell do app para conteúdo

Uma implementação prática

Criamos um exemplo totalmente funcional usando a arquitetura de shell do aplicativo, o JavaScript ES2015 vanilla para o cliente e o Express.js para o servidor. Claro, nada impede que você use sua própria pilha para as partes do cliente ou do servidor (por exemplo, PHP, Ruby, Python).

Ciclo de vida do service worker

Para nosso projeto de shell do aplicativo, usamos o sw-precache, que oferece o seguinte ciclo de vida do worker de serviço:

Evento Ação
Instalar Armazene em cache o shell do aplicativo e outros recursos de apps de página única.
Ativar Limpe os caches antigos.
Buscar Servir um app da Web de página única para URLs e usar o cache para recursos e partes predefinidas. Use a rede para outras solicitações.

Bits do servidor

Nessa arquitetura, um componente do lado do servidor (no nosso caso, escrito em Express) precisa ser capaz de tratar o conteúdo e a apresentação separadamente. O conteúdo pode ser adicionado a um layout HTML que resulta em uma renderização estática da página ou pode ser veiculado separadamente e carregado dinamicamente.

É compreensível que sua configuração do lado do servidor possa ser drasticamente diferente da que usamos no nosso app de demonstração. Esse padrão de apps da Web pode ser alcançado pela maioria das configurações do servidor, embora exija uma nova arquitetura. Descobrimos que o seguinte modelo funciona muito bem:

Diagrama da arquitetura do shell do app
  • Os endpoints são definidos para três partes do aplicativo: os URLs voltados ao usuário (índice/coringa), o shell do aplicativo (service worker) e as partes parciais do HTML.

  • Cada endpoint tem um controlador que puxa um layout de handlebars, que por sua vez pode puxar partes e visualizações de handlebars. Simplificando, as partes são visualizações que são pedaços de HTML copiados para a página final. Observação: os frameworks JavaScript que fazem sincronização de dados mais avançada geralmente são muito mais fáceis de portar para uma arquitetura de shell do aplicativo. Eles tendem a usar vinculação de dados e sincronização em vez de parciais.

  • O usuário recebe inicialmente uma página estática com conteúdo. Esta página registra um service worker, se houver suporte, que armazena em cache o shell do aplicativo e tudo o que ele depende (CSS, JS etc.).

  • O shell do app vai funcionar como um app da Web de página única, usando JavaScript para XHR no conteúdo de um URL específico. As chamadas XHR são feitas para um endpoint /partials*, que retorna o pequeno bloco de HTML, CSS e JS necessário para exibir esse conteúdo. Observação: há muitas maneiras de abordar isso, e o XHR é apenas uma delas. Alguns aplicativos inline os dados (talvez usando JSON) para renderização inicial e, portanto, não são "estáticos" no sentido de HTML achatado.

  • Os navegadores sem suporte a service workers precisam sempre receber uma experiência de fallback. Na nossa demonstração, voltamos à renderização estática básica do lado do servidor, mas essa é apenas uma das muitas opções. O aspecto do worker de serviço oferece novas oportunidades para melhorar a performance do app de estilo de app de página única usando o shell do aplicativo armazenado em cache.

Controle de versões de arquivos

Uma pergunta que surge é como lidar com a versão e a atualização de arquivos. Isso depende do aplicativo, e as opções são:

  • Use a rede primeiro e, caso contrário, use a versão em cache.

  • Somente rede e falha se estiver off-line.

  • Armazenar em cache a versão antiga e atualizar mais tarde.

Para o shell do aplicativo, é necessário usar uma abordagem de cache-first para a configuração do service worker. Se você não estiver armazenando o shell do aplicativo em cache, não adotou a arquitetura corretamente.

Ferramentas

Mantemos várias bibliotecas auxiliares de service worker diferentes que facilitam a configuração do processo de pré-cache do shell do seu app ou o processamento de padrões de cache comuns.

Captura de tela do site da biblioteca de worker de serviço em Web Fundamentals

Usar o sw-precache para o shell do aplicativo

O uso de sw-precache para armazenar em cache o shell do aplicativo deve resolver as questões relacionadas às revisões de arquivo, às perguntas de instalação/ativação e ao cenário de busca do shell do app. Adicione o sw-precache ao processo de build do seu aplicativo e use caracteres curinga configuráveis para coletar os recursos estáticos. Em vez de criar manualmente o script do service worker, deixe que o sw-precache gere um que gerencie seu cache de forma segura e eficiente, usando um gerenciador de busca de cache-primeiro.

As visitas iniciais ao app acionam a pré-cache do conjunto completo de recursos necessários. Isso é semelhante à experiência de instalação de um app nativo de uma app store. Quando os usuários retornam ao app, apenas os recursos atualizados são transferidos por download. Na nossa demonstração, informamos aos usuários quando um novo shell está disponível com a mensagem "Atualizações do app". Atualize para a nova versão." Esse padrão é uma maneira simples de informar aos usuários que eles podem atualizar para a versão mais recente.

Usar a sw-toolbox para armazenamento em cache no ambiente de execução

Use a sw-toolbox para fazer cache de execução com estratégias variadas, dependendo do recurso:

  • cacheFirst para imagens, com um cache nomeado dedicado que tem uma política de expiração personalizada de N maxEntries.

  • networkFirst ou a mais rápida para solicitações de API, dependendo da atualização de conteúdo desejada. O mais rápido pode ser adequado, mas, se houver um feed de API específico que seja atualizado com frequência, use networkFirst.

Conclusão

As arquiteturas de shell de aplicativos têm vários benefícios, mas só fazem sentido para algumas classes de aplicativos. O modelo ainda é novo, e vale a pena avaliar o esforço e os benefícios de desempenho geral dessa arquitetura.

Em nossos experimentos, aproveitamos o compartilhamento de modelos entre o cliente e o servidor para minimizar o trabalho de criação de duas camadas de aplicativo. Isso garante que o aprimoramento progressivo ainda seja um recurso principal.

Se você já está considerando usar service workers no seu app, confira a arquitetura e avalie se ela faz sentido para seus projetos.

Agradecemos aos nossos revisores: Jeff Posnick, Paul Lewis, Alex Russell, Seth Thompson, Rob Dodson, Taylor Savage e Joe Medley.