Como levar os service workers à Pesquisa Google

A história do que foi enviado, como o impacto foi medido e as compensações feitas.

Contexto

Pesquise qualquer tema no Google e você acessa uma página instantaneamente reconhecível de resultados significativos e relevantes. O que você provavelmente não percebeu é que, em determinados cenários, essa página de resultados da pesquisa usa uma tecnologia da Web eficiente chamada de service worker.

Implantar o suporte de service worker para a Pesquisa Google sem impactar negativamente o desempenho exigiu que dezenas de engenheiros trabalhassem em várias equipes. Essa é a história sobre o que foi enviado, como o desempenho foi medido e quais desvantagens foram feitas.

Principais motivos para explorar os service workers

Adicionar um service worker a um app da Web, assim como fazer qualquer mudança de arquitetura no seu site, precisa ser feito com um conjunto claro de metas em mente. Para a equipe do Google Search, havia alguns motivos importantes para testar a adição de um service worker.

Armazenamento limitado em cache dos resultados da pesquisa

A equipe da Pesquisa Google descobriu que é comum que os usuários pesquisem os mesmos termos mais de uma vez em um curto período. Em vez de acionar uma nova solicitação de back-end apenas para extrair os que provavelmente seriam os mesmos resultados, a equipe da Pesquisa quis aproveitar o armazenamento em cache e atender a essas solicitações repetidas localmente.

A importância da atualização não pode ser desconsiderada, e, às vezes, os usuários pesquisam os mesmos termos repetidamente por se tratar de um assunto em evolução e esperam encontrar novos resultados. O uso de um service worker permite que a equipe da Pesquisa implemente uma lógica refinada para controlar o ciclo de vida dos resultados da pesquisa armazenados localmente em cache e alcançar o equilíbrio exato entre velocidade e atualização que, segundo a opinião, atende melhor aos usuários.

Experiência off-line significativa

Além disso, a equipe da Pesquisa Google queria fornecer uma experiência off-line significativa. Para saber mais sobre um tema, o usuário precisa acessar diretamente a página da Pesquisa Google e começar a pesquisar, sem se preocupar com uma conexão de Internet ativa.

Sem um service worker, visitar a página de pesquisa do Google enquanto estiver off-line levaria à página de erro de rede padrão do navegador, e os usuários teriam que se lembrar de voltar e tentar novamente quando a conexão retornasse. Com um service worker, é possível exibir uma resposta HTML off-line personalizada e permitir que os usuários insiram a consulta de pesquisa imediatamente.

Captura de tela da interface de repetição em segundo plano.

Os resultados não estarão disponíveis até que haja uma conexão de Internet, mas o service worker permite que a pesquisa seja adiada e enviada aos servidores do Google assim que o dispositivo ficar on-line novamente usando a API de sincronização em segundo plano.

Exibição e armazenamento em cache do JavaScript mais inteligente

Outra motivação foi otimizar o armazenamento em cache e o carregamento do código JavaScript modularizado que alimenta os vários tipos de recursos na página de resultados da pesquisa. Há uma série de benefícios oferecidos pelo empacotamento JavaScript que fazem sentido quando não há envolvimento de service worker. Por isso, a equipe da Pesquisa não queria simplesmente parar de usar o agrupamento.

Ao usar a capacidade de um service worker de controlar a versão e armazenar em cache partes refinadas de JavaScript no ambiente de execução, a equipe da Pesquisa suspeitava que poderia reduzir a quantidade de rotatividade de cache e garantir que o JavaScript reutilizado no futuro pudesse ser armazenado de forma eficiente. A lógica dentro do service worker pode analisar uma solicitação HTTP de saída de um pacote que contém vários módulos JavaScript e atendê-la reunindo vários módulos localmente armazenados em cache, "desagrupando" quando possível. Isso economiza largura de banda do usuário e melhora a capacidade de resposta geral.

O uso de JavaScript em cache disponibilizado por um service worker também traz benefícios de desempenho: no Chrome, uma representação de código de bytes analisada desse JavaScript é armazenada e reutilizada, o que reduz o trabalho que precisa ser feito no momento da execução para que o JavaScript seja executado na página.

Desafios e soluções

Aqui estão alguns dos obstáculos que precisaram ser superados para alcançar as metas estabelecidas pela equipe. Alguns desses desafios são específicos da Pesquisa Google, mas muitos deles são aplicáveis a uma ampla variedade de sites que podem considerar a implantação de um service worker.

Problema: sobrecarga do service worker

O maior desafio e o verdadeiro obstáculo para o lançamento de um service worker na Pesquisa Google era garantir que ele não fizesse nada que possa aumentar a latência percebida pelo usuário. A Pesquisa Google leva o desempenho muito a sério e, no passado, bloqueou o lançamento de novas funcionalidades que contribuíram até dezenas de milissegundos de latência adicional para um determinado grupo de usuários.

Quando a equipe começou a coletar dados de desempenho durante os primeiros experimentos, surgiu um problema. O HTML retornado em resposta às solicitações de navegação para a página de resultados da pesquisa é dinâmico e varia muito, dependendo da lógica que precisa ser executada nos servidores da Web da Pesquisa. Atualmente, não há como o service worker replicar essa lógica e retornar o HTML armazenado em cache imediatamente. O melhor que ele pode fazer é transmitir solicitações de navegação para os servidores da Web de back-end, o que requer uma solicitação de rede.

Sem um service worker, essa solicitação de rede acontece imediatamente após a navegação do usuário. Quando um service worker é registrado, ele sempre precisa ser iniciado e ter a chance de executar os manipuladores de eventos fetch, mesmo quando não é possível que eles façam nada além de acessar a rede. O tempo necessário para iniciar e executar o código do service worker é uma sobrecarga adicionada sobre cada navegação:

Ilustração da inicialização do SW bloqueando a solicitação de navegação.

Isso coloca a implementação do service worker em desvantagem de latência demais para justificar quaisquer outros benefícios. Além disso, a equipe descobriu que, com base na medição dos tempos de inicialização do service worker em dispositivos reais, havia uma ampla distribuição de tempos de inicialização, com alguns dispositivos móveis mais simples levando quase tanto tempo para iniciar o service worker quanto para fazer a solicitação de rede para o HTML da página de resultados.

Solução: usar o pré-carregamento de navegação

O recurso mais importante que permitiu à equipe da Pesquisa Google avançar com o lançamento do service worker é o pré-carregamento de navegação. O uso do pré-carregamento de navegação é uma melhoria de desempenho importante para qualquer service worker que precisa usar uma resposta da rede para atender a solicitações de navegação. Ela fornece uma dica para o navegador começar a fazer a solicitação de navegação imediatamente, ao mesmo tempo em que o service worker é iniciado:

Ilustração da inicialização do SW feita em paralelo com a solicitação de navegação.

Contanto que o tempo necessário para o service worker iniciar seja menor que o tempo necessário para receber uma resposta da rede, não haverá nenhuma sobrecarga de latência introduzida pelo service worker.

A equipe de Pesquisa também precisava evitar o uso de um service worker em dispositivos móveis de baixo custo, em que o tempo de inicialização do service worker poderia exceder a solicitação de navegação. Como não existe uma regra fixa sobre o que constitui um dispositivo "baixo", eles criaram a heurística de verificar o total de RAM instalado no dispositivo. Qualquer dispositivo com menos de 2 gigabytes de memória se enquadrava na categoria de dispositivo mais simples, em que o tempo de inicialização do service worker seria inaceitável.

É preciso considerar o espaço de armazenamento disponível, já que o conjunto completo de recursos a serem armazenados em cache para uso futuro pode ser executado em vários megabytes. A interface navigator.storage permite que a página da Pesquisa Google descubra com antecedência se as tentativas de armazenar dados em cache correm o risco de falha devido a falhas na cota de armazenamento.

Isso deixava a equipe da Pesquisa com vários critérios para determinar se usaria ou não um service worker: se um usuário acessar a página da Pesquisa Google usando um navegador com suporte para o pré-carregamento de navegação e tiver pelo menos 2 gigabytes de RAM e espaço de armazenamento livre suficiente, um service worker vai ser registrado. Os navegadores ou dispositivos que não atenderem a esses critérios não vão ter um service worker, mas ainda terão a mesma experiência da Pesquisa Google como sempre.

Um benefício desse registro seletivo é a capacidade de enviar um service worker menor e mais eficiente. Direcionar-se a navegadores bastante modernos para executar o código do service worker elimina a sobrecarga de transpilação e polyfills para navegadores mais antigos. Isso acabou eliminando cerca de 8 kilobytes de código JavaScript não compactado do tamanho total da implementação do service worker.

Problema: escopos do service worker

Depois que a equipe da Pesquisa executou experimentos de latência suficientes e estava confiante de que o uso do pré-carregamento de navegação oferecia um caminho viável e neutro em relação à latência para usar um service worker, alguns problemas práticos começaram a ficar em primeiro plano. Um desses problemas está relacionado às regras de escopo do service worker. O escopo de um service worker determina quais páginas ele pode controlar.

O escopo funciona com base no prefixo do caminho do URL. Para domínios que hospedam um único app da Web, isso não é um problema, já que você normalmente usaria apenas um service worker com o escopo máximo de /, que poderia assumir o controle de qualquer página no domínio. Mas a estrutura do URL da Pesquisa Google é um pouco mais complicada.

Se o service worker recebesse o escopo máximo de /, ele poderia assumir o controle de qualquer página hospedada em www.google.com (ou o equivalente regional), e há URLs nesse domínio que não têm relação com a Pesquisa Google. Um escopo mais razoável e restritivo seria /search, que pelo menos eliminaria URLs completamente não relacionados aos resultados da pesquisa.

Infelizmente, mesmo esse caminho de URL /search é compartilhado entre variações diferentes dos resultados da Pesquisa Google, com parâmetros de consulta de URL determinando qual tipo específico de resultado é mostrado. Algumas dessas variações usam bases de código totalmente diferentes da página tradicional de resultados da pesquisa na Web. Por exemplo, a pesquisa por imagens e a pesquisa do Shopping são exibidas no caminho do URL /search com parâmetros de consulta diferentes, mas nenhuma dessas interfaces estava pronta para enviar a própria experiência de service worker (ainda).

Solução: criar uma estrutura de envio e roteamento

Embora existam algumas propostas que permitam algo mais eficiente que os prefixos de caminho de URL para determinar os escopos do service worker, a equipe da Pesquisa Google não conseguiu implantar um service worker que não fez nada em um subconjunto de páginas controladas.

Para contornar esse problema, a equipe da Pesquisa Google criou uma estrutura personalizada de envio e roteamento que poderia ser configurada para verificar critérios, como os parâmetros de consulta da página do cliente, e usá-los para determinar qual caminho de código específico seguir. Em vez de fixar regras no código, o sistema foi criado para ser flexível e permitir que equipes que compartilham o espaço de URL, como pesquisa de imagens e pesquisa do Shopping, coloquem sua própria lógica de service worker no futuro, se decidirem implementá-la.

Problema: métricas e resultados personalizados

Os usuários podem fazer login na Pesquisa usando a Conta do Google, e a experiência dos resultados da pesquisa pode ser personalizada com base nos dados específicos da conta. Os usuários conectados são identificados por cookies do navegador específicos, que são um padrão respeitado e amplamente aceito.

Uma desvantagem de usar cookies do navegador, no entanto, é que eles não são expostos dentro de um service worker, e não há como examinar automaticamente seus valores e garantir que eles não sejam alterados por causa de um usuário sair ou alternar entre contas. Há um esforço em andamento para trazer o acesso a cookies para service workers, mas, no momento, a abordagem é experimental e não tem suporte amplo.

Uma incompatibilidade entre a visualização do service worker do usuário conectado e o usuário real conectado na interface da Web da Pesquisa Google pode levar a resultados de pesquisa personalizados incorretamente ou a métricas e registros atribuídos incorretamente. Qualquer um desses cenários de falha seria um problema sério para a equipe da Pesquisa Google.

Solução: enviar cookies usando postMessage

Em vez de esperar que APIs experimentais sejam iniciadas e forneçam acesso direto aos cookies do navegador dentro de um service worker, a equipe da Pesquisa Google optou por uma solução temporária: sempre que uma página controlada pelo service worker é carregada, a página lê os cookies relevantes e usa postMessage() para enviá-los ao service worker.

Em seguida, o service worker verifica o valor atual do cookie em relação ao valor esperado e, se houver uma incompatibilidade, ele toma medidas para limpar todos os dados específicos do usuário do armazenamento e recarrega a página de resultados da pesquisa sem qualquer personalização incorreta.

As etapas específicas que o service worker segue para redefinir as coisas para um valor de referência são específicas para os requisitos da Pesquisa Google, mas a mesma abordagem geral pode ser útil para outros desenvolvedores que lidam com dados personalizados codificados fora dos cookies do navegador.

Problema: experimentos e dinamismo

Como mencionado, a equipe da Pesquisa Google depende muito da realização de experimentos na produção e teste dos efeitos de novos códigos e recursos no mundo real antes de ativá-los por padrão. Isso pode ser um pouco desafiador com um service worker estático que depende muito de dados armazenados em cache, já que ativar e sair de experimentos geralmente requer comunicação com o servidor de back-end.

Solução: script do service worker gerado dinamicamente

A solução que a equipe escolheu foi usar um script de service worker gerado dinamicamente, personalizado pelo servidor da Web para cada usuário, em vez de um único script de service worker estático que é gerado antecipadamente. As informações sobre experimentos que podem afetar o comportamento do service worker ou as solicitações de rede em geral estão incluídas diretamente nesses scripts personalizados. A alteração dos conjuntos de experiências ativas para um usuário é feita por meio de uma combinação de técnicas tradicionais, como cookies de navegador, além da exibição do código atualizado no URL do service worker registrado.

O uso de um script de service worker gerado dinamicamente também facilita o fornecimento de uma saída no caso improvável de uma implementação de service worker ter um bug fatal que precisa ser evitado. A resposta do worker do servidor dinâmico pode ser uma implementação de ambiente autônomo, desativando efetivamente o service worker para alguns ou todos os usuários atuais.

Problema: coordenando atualizações

Um dos desafios mais difíceis enfrentados por qualquer implantação de service worker do mundo real é encontrar um equilíbrio razoável entre evitar a rede em favor do cache e, ao mesmo tempo, garantir que os usuários atuais recebam atualizações e alterações críticas logo após a implantação na produção. O equilíbrio certo depende de muitos fatores:

  • Indica se o app da Web é um app de página única de longa duração que o usuário mantém aberto indefinidamente, sem navegar para novas páginas.
  • Qual é a cadência de implantação das atualizações no servidor da Web de back-end.
  • Se o usuário médio tolerar o uso de uma versão ligeiramente desatualizada do seu app da Web ou se a atualização é a maior prioridade.

Durante os testes com os service workers, a equipe da Pesquisa Google manteve os experimentos em execução em várias atualizações de back-end programadas para garantir que as métricas e a experiência do usuário corresponderiam melhor ao que os usuários retornantes veriam no mundo real.

Solução: equilibrar a atualização e a utilização do cache

Depois de testar várias opções de configuração diferentes, a equipe da Pesquisa Google descobriu que a configuração a seguir fornecia o equilíbrio certo entre atualização e utilização do cache.

O URL do script do service worker é exibido com o cabeçalho de resposta Cache-Control: private, max-age=1500 (1.500 segundos ou 25 minutos) e é registrado com updateViaCache definido como "all" para garantir que o cabeçalho seja respeitado. O back-end da Web da Pesquisa Google é, como você pode imaginar, um grande conjunto de servidores distribuídos globalmente que requer o máximo de tempo de atividade possível de 100%. A implantação de uma alteração que afetaria o conteúdo do script do service worker é feita de maneira contínua.

Se um usuário acessar um back-end que já foi atualizado e navegar rapidamente para outra página que ainda não recebeu o service worker atualizado, ele vai alternar entre as versões várias vezes. Portanto, informar ao navegador para verificar se há um script atualizado apenas se 25 minutos se passarem desde que a última verificação não tem uma desvantagem significativa. A vantagem de ativar esse comportamento é reduzir significativamente o tráfego recebido pelo endpoint que gera dinamicamente o script do service worker.

Além disso, um cabeçalho ETag é definido na resposta HTTP do script do service worker, garantindo que, quando uma verificação de atualização for feita após 25 minutos, o servidor poderá responder de maneira eficiente com uma resposta HTTP 304 se não houver atualizações no service worker implantado nesse período.

Ainda que algumas interações no app da Web da Pesquisa Google usem navegações em estilo de app de página única (ou seja, usando a API History), na maioria das vezes, a Pesquisa Google é um app da Web tradicional que usa navegações "reais". Isso entra em jogo quando a equipe decidiu que seria eficaz usar duas opções que aceleram o ciclo de vida de atualização do service worker: clients.claim() e skipWaiting(). Clicar na interface da Pesquisa Google geralmente leva a novos documentos HTML. Chamar skipWaiting garante que um service worker atualizado tenha a chance de processar essas novas solicitações de navegação imediatamente após a instalação. Da mesma forma, chamar clients.claim() significa que o service worker atualizado tem a chance de começar a controlar qualquer página aberta da Pesquisa Google que não seja controlada, após a ativação do service worker.

A abordagem adotada pela Pesquisa Google não é necessariamente uma solução que funciona para todos. Ela foi o resultado de um cuidadosamente teste A/B de várias combinações de opções de veiculação até descobrir o que funcionava melhor para elas. Os desenvolvedores com infraestrutura de back-end que permitem implantar atualizações mais rapidamente podem preferir que o navegador verifique se há um script de service worker atualizado com a maior frequência possível, sempre ignorando o cache HTTP. Se você estiver criando um app de página única que os usuários vão manter aberto por um longo período, o uso de skipWaiting() provavelmente não é a escolha certa para você. Você correrá o risco de encontrar inconsistências de cache se permitir que o novo service worker seja ativado enquanto houver clientes de longa duração.

Conclusões importantes

Por padrão, os service workers não são neutros em relação ao desempenho

Adicionar um service worker ao app da Web significa inserir outra parte do JavaScript que precisa ser carregada e executada antes que o app da Web receba respostas às solicitações. Se essas respostas vêm de um cache local e não da rede, a sobrecarga da execução do service worker geralmente é insignificante em comparação com a melhora no desempenho de o armazenamento em cache. No entanto, se você sabe que o service worker sempre precisa consultar a rede ao processar solicitações de navegação, o uso do pré-carregamento de navegação é uma melhoria de desempenho crucial.

Os service workers são (ainda) um aprimoramento progressivo

A história de suporte dos service workers é muito mais brilhante hoje do que há um ano. Todos os navegadores modernos agora apresentam pelo menos algum suporte a service workers, mas, infelizmente, há alguns recursos avançados do service worker, como sincronização em segundo plano e pré-carregamento de navegação, que não são implementados universalmente. A verificação de atributos para o subconjunto específico de recursos que você sabe que precisa e o registro de um service worker somente quando eles estão presentes ainda é uma abordagem razoável.

Da mesma forma, se você realizou experimentos gerais e sabe que dispositivos mais simples têm um desempenho ruim com a sobrecarga adicional de um service worker, também poderá evitar o registro de um service worker nesses cenários.

Continue a tratar os service workers como um aprimoramento progressivo que é adicionado a um app da Web quando todos os pré-requisitos são atendidos e o service worker adiciona algo positivo à experiência do usuário e ao desempenho geral de carregamento.

Medir tudo

A única maneira de descobrir se o envio de um service worker teve um impacto positivo ou negativo nas experiências dos usuários é testar e medir os resultados.

As especificidades da configuração de medições significativas dependem do provedor de análise usado e de como você normalmente conduz experimentos na configuração de implantação. Uma abordagem, usando o Google Analytics para coletar métricas, é detalhada neste estudo de caso com base na experiência com service workers no app da Web do Google I/O.

Sem metas

Embora muitos membros da comunidade de desenvolvimento da Web associem service workers a Progressive Web Apps, criar um "PWA da Pesquisa Google" não era um objetivo inicial da equipe. No momento, o app da Web da Pesquisa Google não fornece metadados por um manifesto de app da Web nem incentiva os usuários a passar pelo fluxo "Adicionar à tela inicial". Atualmente, a equipe da Pesquisa está satisfeita com os usuários que acessam o app da Web pelos pontos de entrada tradicionais da Pesquisa Google.

Em vez de tentar transformar a experiência da Pesquisa Google na Web no equivalente ao que você esperaria de um aplicativo instalado, o foco no lançamento inicial foi melhorar progressivamente o site atual.

Agradecimentos

Agradecemos a toda a equipe de desenvolvimento da Web da Pesquisa Google pelo trabalho na implementação do service worker e por compartilhar o material de referência que foi necessário para escrever este artigo. Um agradecimento particular a Philippe Golle, Rajesh Jagannathan, R. Samuel Klatchko, Andy Martone, Leonardo Peña, Rachel Shearer, Greg Terrono e Clay Woolam.

Atualização (outubro de 2021): desde que este artigo foi publicado, a equipe da Pesquisa Google reavaliou os benefícios e as desvantagens da arquitetura atual de service worker. O service worker descrito acima está sendo descontinuado. À medida que a infraestrutura da Web da Pesquisa Google evolui, a equipe pode revisitar o design dos service workers.