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

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

  • carregar rapidamente
  • ser armazenado em cache
  • exibir conteúdo dinamicamente

Um shell de aplicativo é o segredo para um desempenho satisfatório e confiável. O shell do seu app é como um pacote de código que você publicaria em uma app store se estivesse criando um app nativo. Essa é a carga necessária para começar, mas talvez ela não seja o todo. Ele mantém a IU local e extrai conteúdo de maneira dinâmica com uma API.

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

Contexto

O artigo Progressive Web Apps, de Alex Russell, descreve como um app da Web pode mudar progressivamente com o uso e o consentimento do usuário para fornecer 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 da funcionalidade e dos benefícios de desempenho do service worker e da capacidade de armazenamento em cache. Isso permite que você se concentre na velocidade, garantindo aos seus apps da Web o mesmo carregamento instantâneo e atualizações regulares que você costuma ver em aplicativos nativos.

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

Vamos nos aprofundar em como estruturar seu app usando uma arquitetura de shell de aplicativo aumentada pelo service worker. Vamos analisar a renderização do lado do cliente e do servidor e compartilhar um exemplo completo para você testar.

Para enfatizar esse ponto, o exemplo abaixo mostra o primeiro carregamento de um app usando essa arquitetura. Observe o aviso "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, poderemos solicitar que o usuário atualize para a nova versão.

Imagem do service worker em execução no DevTools para o shell do aplicativo

Aliás, o que são service workers?

Um service worker é um script executado em segundo plano, separado da página da Web. Ele responde a eventos, incluindo solicitações de rede feitas a partir de páginas que exibe e envia avisos do seu servidor. Um service worker tem uma vida útil intencionalmente curta. Ele desperta ao receber um evento e permanece em execução apenas pelo tempo necessário para processá-lo.

Os service workers também têm um conjunto limitado de APIs quando comparados ao JavaScript em um contexto de navegação normal. Esse é o padrão para os workers na Web. Um service worker não pode acessar o DOM, mas pode acessar coisas como a API Cache e pode fazer solicitações de rede usando a API Fetch. A IndexedDB API e 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 que ele controla. Os eventos de push enviados do seu servidor podem invocar a API de notificação para aumentar o engajamento do usuário.

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

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

Benefícios de desempenho

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

Em visitas repetidas, isso permite que você mostre pixels significativos na tela sem a rede, mesmo que o conteúdo venha dela. É como exibir barras de ferramentas e cards imediatamente e, em seguida, carregar o restante do conteúdo de forma progressiva.

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

Teste 1: testes a cabo com um Nexus 5 usando o Chrome Dev

A primeira visualização do app precisa buscar todos os recursos da rede e só atinge uma pintura significativa em 1,2 segundo. Graças ao armazenamento em cache do service worker, nossa visita repetida alcança uma exibição significativa e termina de carregar em 0,5 segundo.

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

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

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

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

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

Pinte a linha do tempo para a primeira visualização no teste da página da Web

ao 0,9 segundo necessário quando a mesma página é carregada do cache do nosso service worker. Mais de dois segundos de tempo são salvos para nossos usuários finais.

Pinte a linha do tempo para visualização repetida no teste da página da Web

Ganhos de desempenho semelhantes e confiáveis são possíveis para seus próprios aplicativos usando a arquitetura de shell do aplicativo.

O service worker exige que repensemos a forma como estruturamos os aplicativos?

Os service workers implicam algumas mudanças sutis na arquitetura do aplicativo. Em vez de colocar todo o seu aplicativo em uma string HTML, pode ser vantajoso usar o estilo AJAX. É aqui que você tem um shell (que sempre é armazenado em cache e pode ser inicializado sem a rede) e um conteúdo que é atualizado regularmente e gerenciado separadamente.

Essa divisão tem grandes implicações. Na primeira visita, você pode renderizar conteúdo no servidor e instalar o service worker no cliente. Nas visitas subsequentes, você só precisará solicitar dados.

E quanto ao aprimoramento progressivo?

Embora o service worker não seja compatível com todos os navegadores, a arquitetura de shell de conteúdo do aplicativo usa aprimoramento progressivo para garantir que todos possam acessar o conteúdo. Por exemplo, vamos usar nosso projeto de exemplo.

Confira abaixo a versão completa renderizada no Chrome, Firefox Nightly e Safari. À esquerda, veja a versão do Safari em que o conteúdo é renderizado no servidor sem um service worker. À direita, vemos as versões noturnas do Chrome e do Firefox com a tecnologia de service worker.

Imagem do Application Shell carregado no Safari, Chrome e Firefox

Quando faz sentido usar essa arquitetura?

A arquitetura de shell do aplicativo é mais adequada para aplicativos e sites que são dinâmicos. Se o site for pequeno e estático, provavelmente não será necessário um shell de aplicativo. Basta armazenar todo o site em cache em uma etapa oninstall do service worker. Use a abordagem que faz mais sentido para seu projeto. Várias estruturas de JavaScript já incentivam a divisão da lógica do aplicativo do conteúdo, tornando a aplicação desse padrão mais simples.

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

A arquitetura de shell do aplicativo é possível com apenas algumas alterações na interface de usuário geral do seu aplicativo e tem funcionado bem em sites de grande escala, como o Progressive Web App 2015 do Google e a caixa de entrada do Google.

Imagem da caixa de entrada do Google carregando. Ilustração da Caixa de entrada usando um service worker.

Shells de aplicativos off-line são uma grande vantagem de desempenho e também aparecem bem no app off-line da Wikipédia de Jake Archibald e no Progressive Web App do Flipkart Lite.

Capturas de tela da demonstração da Wikipédia de Jake Archibald.

Explicação da arquitetura

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

Primeiro carregamento e outras páginas

Diagrama do primeiro carregamento com o shell do app

Em geral, a arquitetura de shell do aplicativo vai:

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

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

  • Use ferramentas de service worker, como o sw-precache, para armazenar em cache e atualizar de maneira confiável o service worker que gerencia seu conteúdo estático. (falaremos sobre o sw-precache mais tarde).

Para fazer isso:

  • Server: enviará conteúdo HTML que o cliente pode renderizar e usará cabeçalhos de expiração do cache HTTP muito futuros para considerar navegadores sem suporte a service worker. Ele disponibilizará nomes de arquivos usando hashes para possibilitar o "controle de versões" e as atualizações fáceis para mais tarde no ciclo de vida do aplicativo.

  • As páginas vão incluir estilos CSS inline em uma tag <style> no <head> do documento para fornecer uma first paint rápida do shell do aplicativo. Cada página carregará de maneira assíncrona o JavaScript necessário para a visualização atual. Como o CSS não pode ser carregado de maneira assíncrona, podemos solicitar estilos usando JavaScript, pois ele é assíncrono em vez de orientado por analisador e síncrono. Também podemos aproveitar o requestAnimationFrame() para evitar casos em que podemos receber uma ocorrência rápida em cache e fazer com que os estilos se tornem parte acidentalmente do caminho crítico de renderização. requestAnimationFrame() força o primeiro frame a ser pintado antes do carregamento dos estilos. Outra opção é usar projetos como o loadCSS do Filament Group para solicitar CSS de maneira assíncrona usando JavaScript.

  • O service worker armazenará uma entrada em cache do shell do aplicativo para que, em visitas repetidas, o shell possa ser carregado inteiramente a partir 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 vanilla ES2015 para o cliente e o Express.js para o servidor. Obviamente, 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 sw-precache, que oferece o seguinte ciclo de vida de service worker:

Evento Ação
Instalar Armazene em cache o shell do aplicativo e outros recursos de aplicativo de página única.
Ativar Limpe os caches antigos.
Fetch Disponibilize um aplicativo da Web de uma só página para URLs e use o cache para recursos e parciais predefinidos. Usar rede para outras solicitações.

Bits de servidor

Nessa arquitetura, um componente do lado do servidor (no nosso caso, escrito em Express) deve 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 ser veiculado separadamente e carregado dinamicamente.

É compreensível que sua configuração do lado do servidor seja 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 de servidor, embora exija alguma rearquitetura. Descobrimos que o modelo a seguir funciona muito bem:

Diagrama da arquitetura do shell do app
  • Os endpoints são definidos em três partes do aplicativo: o URL voltado ao usuário (índice/curinga), o shell do aplicativo (service worker) e as partes HTML.

  • Cada endpoint tem um controlador que extrai um layout de manipuladores que, por sua vez, podem extrair partes e visualizações parciais do guidão. Simplificando, parciais são visualizações que são pedaços de HTML copiados para a página final. Observação: frameworks JavaScript que fazem sincronização de dados mais avançadas geralmente são mais fáceis de transferir para uma arquitetura de shell de aplicativo. Eles tendem a usar vinculação e sincronização de dados em vez de parciais.

  • Inicialmente, o usuário recebe 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 de que ele depende (CSS, JS etc.).

  • O shell do aplicativo atuará como um aplicativo 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 a pequena parte 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 colocarão seus dados em linha (talvez usando JSON) para a renderização inicial e, portanto, não são "estáticos" no sentido HTML simples.

  • Navegadores sem suporte a service workers devem sempre ter uma experiência alternativa. Em nossa demonstração, vamos voltar para a renderização estática básica do lado do servidor, mas essa é apenas uma das muitas opções. O aspecto do service worker oferece novas oportunidades para melhorar o desempenho do seu app de estilo de aplicativo de página única usando o shell de aplicativo armazenado em cache.

Controle de versão do arquivo

Uma questão que se surge é como lidar com o controle de versões e a atualização de arquivos. Ele é específico do aplicativo e as opções são:

  • Rede primeiro. Caso contrário, use a versão em cache.

  • Apenas rede. Falha se off-line.

  • Armazene a versão antiga em cache e atualize mais tarde.

Para o próprio shell do aplicativo, uma abordagem que prioriza o cache deve ser adotada para a configuração do service worker. Se não estiver armazenando o shell do aplicativo em cache, você não adotou a arquitetura corretamente.

Ferramentas

Mantemos diversas bibliotecas auxiliares de service workers que facilitam a configuração do processo de armazenamento prévio em cache do shell do seu aplicativo ou processamento de padrões comuns de armazenamento em cache.

Captura de tela do site da biblioteca Service Worker nos fundamentos da Web

Usar sw-precache para o shell do seu aplicativo

Usar sw-precache para armazenar o shell do aplicativo em cache deve resolver as preocupações relacionadas a revisões de arquivos, perguntas de instalação/ativação e o cenário de busca do shell do app. Insira o sw-precache no processo de build do aplicativo e use caracteres curinga configuráveis para selecionar recursos estáticos. Em vez de criar manualmente o script do service worker, deixe o sw-precache gerar um gerenciador de busca seguro e eficiente usando um gerenciador de busca que prioriza o cache.

As visitas iniciais ao seu app acionam o pré-armazenamento em cache do conjunto completo de recursos necessários. Isso é semelhante à experiência de instalar um aplicativo nativo de uma app store. Quando os usuários retornam ao seu 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 o sw-toolkit para armazenamento em cache no momento da execução

Use sw-toolbox para armazenamento em cache do ambiente de execução com estratégias variadas dependendo do recurso:

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

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

Conclusão

As arquiteturas de shell de aplicativo oferecem vários benefícios, mas só fazem sentido para algumas classes de aplicativos. O modelo ainda é jovem e valerá a pena avaliar os benefícios do esforço e do 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 importante.

Se você já está pensando em usar service workers no seu aplicativo, dê uma olhada na arquitetura e avalie se ela faz sentido para seus próprios projetos.

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