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

Addy Osmani
Addy Osmani

Um shell do aplicativo é o código HTML, CSS e JavaScript mínimo que define a interface do usuário. O shell do aplicativo precisa:

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

Um shell do aplicativo é o segredo para um desempenho eficiente e confiável. Pense no shell do seu 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 o início, mas pode não ser o todo. Ele mantém sua IU local e extrai conteúdo dinamicamente por meio de uma API.

Separação do shell do aplicativo entre o shell do HTML, JS e 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 oferecer uma experiência mais semelhante ao 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 os recursos de armazenamento em cache dele. Isso permite que você se concentre na velocidade, proporcionando aos seus apps da Web o mesmo carregamento instantâneo e as atualizações regulares que você costuma ver em aplicativos nativos.

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

Vamos nos aprofundar em como estruturar o app usando uma arquitetura de shell de aplicativo aumentada por worker de serviço. Vamos analisar a renderização do lado do cliente e do servidor e compartilhar uma amostra completa que você pode testar hoje mesmo.

Para enfatizar esse ponto, o exemplo abaixo mostra o primeiro carregamento de um app usando essa arquitetura. O aviso "O app está pronto para uso off-line" aparece na parte de baixo da tela. Se uma atualização do shell for disponibilizada posteriormente, informaremos ao usuário que é necessário atualizar para a nova versão.

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

O que são os 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 nas páginas exibidas e avisos push do servidor. Um service worker tem uma vida útil intencionalmente curta. Ele desperta quando recebe um evento e é executado apenas durante o 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. Isso é padrão para os workers na Web. Um service worker não pode acessar o DOM, mas pode acessar itens como a API Cache e fazer solicitações de rede usando a API Fetch. A API IndexedDB e postMessage() também estão disponíveis para uso na persistência de dados e no envio de mensagens entre o service worker e as páginas que ele controla. Os eventos push enviados do seu servidor podem invocar a API Notification para aumentar o engajamento do usuário.

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

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

Benefícios de desempenho

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

Isso permite que você mostre pixels significativos na tela sem a rede, mesmo que seu conteúdo venha dela em acessos repetidos. Pense nisso como a exibição de barras de ferramentas e cards imediatamente e, em seguida, o carregamento do restante do conteúdo progressivamente.

Para testar essa arquitetura em dispositivos reais, executamos nosso exemplo de shell do aplicativo em WebPageTest.org (link em inglês) e mostramos os resultados abaixo.

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

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

Diagrama de exibição de teste da página da Web para conexão de 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. Desta vez, a primeira exibição significativa leva 2,5 segundos na primeira visita. Leva 7,1 segundos para carregar a página totalmente. Com o armazenamento em cache do service worker, nossa visita repetida tem uma exibição significativa e termina o carregamento em 0,8 segundo.

Diagrama de exibição de teste da 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 exibição significativa no shell do aplicativo:

Linha do tempo de exibição da primeira visualização do teste da página da Web

até o 0,9 segundo que leva quando a mesma página é carregada a partir do cache do nosso service worker. Mais de dois segundos são economizados para nossos usuários finais.

Linha do tempo de exibição para visualização repetida do teste da página da Web

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

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

Service workers implicam algumas mudanças sutis na arquitetura dos aplicativos. Em vez de comprimir todo o seu aplicativo em uma string HTML, pode ser benéfico 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.

As implicações dessa divisão são grandes. No primeiro acesso, é possível renderizar conteúdo no servidor e instalar o service worker no cliente. Nas visitas subsequentes, você precisará solicitar apenas os dados.

E o aprimoramento progressivo?

Embora no momento nem todos os navegadores tenham suporte para o service worker, a arquitetura de shell de conteúdo do aplicativo usa aprimoramento progressivo para garantir que todos possam acessar o conteúdo. Por exemplo, vamos ao nosso exemplo de projeto.

Abaixo, você pode ver a versão completa renderizada no Chrome, no Firefox Nightly e no Safari. À esquerda, é possível ver a versão do Safari em que o conteúdo é renderizado no servidor sem um service worker. À direita, vemos as versões do Chrome e do Firefox Nightly com a tecnologia do service worker.

Imagem de um shell do aplicativo carregado nos navegadores Safari, Chrome e Firefox

Quando faz sentido usar essa arquitetura?

A arquitetura de shell do aplicativo é ideal para apps e sites dinâmicos. Caso seu site seja pequeno e estático, você provavelmente não vai precisar de um shell de aplicativo e pode simplesmente armazenar o site inteiro em cache em uma etapa oninstall do service worker. Use a abordagem que faz mais sentido para o seu projeto. Várias estruturas de JavaScript já incentivam a divisão da lógica do aplicativo do conteúdo, tornando esse padrão mais direto para a aplicação.

Algum app de produção já usa esse padrão?

A arquitetura de shell do aplicativo é possível com apenas algumas mudanças na IU geral do seu aplicativo e funciona bem em sites de grande escala, como o Progressive Web App do Google I/O 2015 e a Caixa de entrada do Google.

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

Shells de aplicativos off-line são uma grande melhora de desempenho e também são bem demonstrados 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 é exibir conteúdo significativo na tela do usuário o mais rápido possível.

Primeiro carregue e carregue outras páginas

Diagrama do primeiro carregamento com o shell do app

Em geral, a arquitetura de shell do aplicativo:

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

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

  • Use ferramentas de service worker, como sw-precache, por exemplo, para armazenar em cache e atualizar de maneira confiável o service worker que gerencia seu conteúdo estático. Veja mais informações sobre o sw-precache posteriormente.

Para fazer isso:

  • O servidor envia conteúdo HTML que o cliente pode renderizar e usa cabeçalhos de expiração do cache HTTP distantes para dar conta dos navegadores sem suporte ao service worker. Ele vai veicular nomes de arquivos usando hashes para permitir o "controle de versões" e 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 documento <head> para oferecer uma primeira exibição rápida do shell do aplicativo. Cada página 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, é possível solicitar estilos usando o JavaScript, já que ele é assíncrono, em vez de ser síncrono e orientado por analisadores. Também podemos aproveitar o requestAnimationFrame() para evitar casos em que uma ocorrência rápida em cache pode ser atingida, fazendo com que os estilos se tornem acidentalmente parte do caminho crítico de renderização. requestAnimationFrame() força o primeiro frame a ser pintado antes que os estilos sejam carregados. Outra opção é usar projetos como o loadCSS do Filament Group para solicitar CSS de forma assíncrona usando JavaScript.

  • O service worker vai armazenar uma entrada do shell do aplicativo em cache para que, em acessos repetidos, o shell possa ser carregado inteiramente no cache do service worker, a menos que haja uma atualização 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, JavaScript baunilha ES2015 para o cliente e Express.js para o servidor. Não há nada que impeça você de usar sua própria pilha para o cliente ou as partes 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 do service worker:

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.
Busca Exiba um app da Web de página única para URLs e use o cache para recursos e parciais predefinidos. Use a 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 exibido separadamente e carregado dinamicamente.

A configuração no lado do servidor pode ser muito diferente da usada 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 seja necessário reformular a arquitetura. Descobrimos que o seguinte modelo funciona muito bem:

Diagrama da arquitetura de shell do app
  • Os endpoints são definidos para três partes de seu aplicativo: o URL direcionado ao usuário (índice/caractere curinga), o shell do aplicativo (service worker) e seus parciais HTML.

  • Cada endpoint tem um controlador que extrai um layout de identificadores, que, por sua vez, pode incluir visualizações e parciais do guidão. Simplificando, parciais são exibiçõ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çada costumam ser mais fáceis de transferir para uma arquitetura de shell do 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 o shell do aplicativo em cache e tudo de que depende (CSS, JS etc.).

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

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

Controle de versões de arquivos

Uma pergunta que surge é sobre como lidar com o controle de versões e a atualização de arquivos. Essa opção é específica do aplicativo. As opções são:

  • Rede e usar a versão em cache caso contrário.

  • Somente rede e falha se off-line.

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

Para o shell do aplicativo, é necessário adotar uma abordagem que priorize o cache para a configuração do service worker. Se você não está armazenando o shell do aplicativo em cache, isso significa que não adotou a arquitetura corretamente.

Ferramentas

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

Captura de tela do site da biblioteca Service Worker no Web Fundamentals

Usar sw-precache para o shell do seu aplicativo

O uso de sw-precache para armazenar o shell do aplicativo em cache deve lidar com as preocupações relacionadas às revisões de arquivos, as perguntas de instalação/ativação e o cenário de busca do shell do aplicativo. Coloque sw-precache no processo de compilação do aplicativo e use caracteres curinga configuráveis para coletar seus recursos estáticos. Em vez de criar manualmente o script do service worker, deixe que o sw-precache gere um código que gerencie o cache de maneira segura e eficiente, usando um gerenciador de busca que prioriza o cache.

As visitas iniciais ao aplicativo acionam o pré-armazenamento em cache do conjunto completo de recursos necessários. Esse processo é semelhante à instalação de um aplicativo nativo de uma app store. Quando os usuários retornam ao seu app, somente os recursos atualizados são transferidos por download. Na 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 sw-toolkit para armazenamento em cache no ambiente de execução

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

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

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

Conclusão

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

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

Se você já está pensando em usar service workers no seu app, 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.