Descubra o que é o scanner de pré-carregamento do navegador, como ele melhora o desempenho e como você pode se afastar dele.
Um aspecto negligenciado da otimização da velocidade de uma página envolve saber um pouco sobre os aspectos internos do navegador. Os navegadores fazem certas otimizações para melhorar o desempenho de maneiras que nós, desenvolvedores, não podemos, mas apenas contanto que essas otimizações não sejam interrompidas involuntariamente.
Uma otimização interna do navegador que você precisa entender é o verificador de pré-carregamento do navegador. Esta postagem abordará como funciona o scanner de pré-carregamento e, o mais importante, como você pode evitar qualquer problema.
O que é um scanner de pré-carregamento?
Todo navegador tem um analisador HTML primário que tokeniza a marcação bruta e a processa em um modelo de objeto. Isso acontece até que o analisador faça uma pausa ao encontrar um recurso de bloqueio, como uma folha de estilo carregada com um elemento <link>
ou um script carregado com um elemento <script>
sem um atributo async
ou defer
.
No caso de arquivos CSS, a análise e a renderização são bloqueadas para evitar flash de conteúdo sem estilo (FOUC, na sigla em inglês). Isso ocorre quando uma versão sem estilo de uma página pode ser vista brevemente antes da aplicação de estilos.
O navegador também bloqueia a análise e a renderização da página quando encontra elementos <script>
sem um atributo defer
ou async
.
Isso acontece porque o navegador não tem certeza se um determinado script modificará o DOM enquanto o analisador de HTML principal ainda estiver fazendo seu trabalho. É por isso que tem sido uma prática comum carregar seu JavaScript no final do documento para que os efeitos de análise e renderização bloqueadas se tornem marginais.
Estes são bons motivos para o navegador deve bloquear a análise e a renderização. No entanto, bloquear qualquer uma dessas etapas importantes não é desejável, porque elas podem atrasar a descoberta de outros recursos importantes para atrasar a exibição. Felizmente, os navegadores fazem o possível para reduzir esses problemas usando um analisador HTML secundário chamado verificador de pré-carregamento.
O papel de um scanner de pré-carregamento é especulativo, ou seja, ele examina a marcação bruta para encontrar recursos a serem buscados de forma oportuna antes que o analisador HTML principal as descubra.
Como saber quando o scanner de pré-carregamento está funcionando
O scanner de pré-carregamento existe por causa de renderização e análise bloqueadas. Se esses dois problemas de desempenho nunca existiram, o scanner de pré-carregamento não será muito útil. O segredo para descobrir se uma página da Web se beneficia do scanner de pré-carregamento depende desses fenômenos de bloqueio. Para fazer isso, você pode introduzir um atraso artificial às solicitações para descobrir onde o scanner de pré-carregamento está funcionando.
Veja esta página de texto e imagens básicas com uma folha de estilo como exemplo. Como os arquivos CSS bloqueiam a renderização e a análise, você introduz um atraso artificial de dois segundos para a folha de estilo por meio de um serviço de proxy. Esse atraso facilita a visualização na hierarquia da rede em que o scanner de pré-carregamento está funcionando.
Como é possível conferir na hierarquia, o scanner de pré-carregamento descobre o elemento <img>
mesmo quando a renderização e a análise do documento estão bloqueadas. Sem essa otimização, o navegador não pode buscar itens de forma oportuna durante o período de bloqueio, e mais solicitações de recursos seriam consecutivas em vez de simultâneas.
Com esse exemplo do brinquedo pronto, vamos analisar alguns padrões reais em que o leitor de pré-carregamento pode ser derrotado — e o que pode ser feito para corrigi-lo.
Scripts async
injetados
Digamos que você tenha um HTML no <head>
que inclua um JavaScript inline como este:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Os scripts injetados são async
por padrão. Quando eles são injetados, eles se comportam como se o atributo async
tivesse sido aplicado a ele. Isso significa que ela será executada o mais rápido possível e não bloqueará a renderização. Parece ideal, certo? No entanto, se você presumir que essa <script>
inline vem depois de um elemento <link>
que carrega um arquivo CSS externo, você vai ter um resultado abaixo do ideal:
Vamos detalhar o que aconteceu aqui:
- Em 0 segundo, o documento principal é solicitado.
- Em 1,4 segundo, chega o primeiro byte da solicitação de navegação.
- Em 2 segundos, o CSS e a imagem são solicitados.
- Como o analisador está bloqueado ao carregar a folha de estilo e o JavaScript in-line que injeta o script
async
vem depois dessa folha em 2,6 segundos, a funcionalidade que o script fornece não está disponível o quanto antes.
Isso não é ideal porque a solicitação do script ocorre somente após a conclusão do download da folha de estilo. Isso atrasa a execução do script o mais rápido possível. Por outro lado, como o elemento <img>
pode ser encontrado na marcação fornecida pelo servidor, ele será descoberto pelo scanner de pré-carregamento.
O que acontece se você usar uma tag <script>
normal com o atributo async
em vez de injetar o script no DOM?
<script src="/yall.min.js" async></script>
Este é o resultado:
Pode haver alguma tentação de sugerir que esses problemas podem ser resolvidos usando rel=preload
. Isso certamente funciona, mas pode acarretar alguns efeitos colaterais. Afinal, por que usar rel=preload
para corrigir um problema que pode ser evitado não injetando um elemento <script>
no DOM?
O pré-carregamento "corrige" o problema aqui, mas apresenta um novo problema: o script async
nas duas primeiras demonstrações, apesar de estar carregado na <head>
, é carregado na prioridade "Baixa", enquanto a folha de estilo é carregada na prioridade "Mais alta". Na última demonstração, em que o script async
é pré-carregado, a folha de estilo ainda é carregada na prioridade "Mais alta", mas a prioridade do script foi promovida para "Alta".
Quando a prioridade de um recurso é elevada, o navegador aloca mais largura de banda a ele. Isso significa que, mesmo que a folha de estilo tenha a prioridade mais alta, a prioridade elevada do script pode causar contenção de largura de banda. Isso pode ser um fator em conexões lentas ou em casos em que os recursos são muito grandes.
A resposta é direta: se for necessário um script durante a inicialização, não invalide o scanner de pré-carregamento injetando-o no DOM. Faça testes (conforme necessário) com o posicionamento do elemento <script>
e com atributos como defer
e async
.
Carregamento lento com JavaScript
O carregamento lento é um ótimo método de conservação de dados, muito usado em imagens. No entanto, às vezes, o carregamento lento é aplicado incorretamente a imagens que estão "acima da dobra".
Isso apresenta possíveis problemas com a detecção de recursos em que o verificador de pré-carregamento está relacionado e pode atrasar desnecessariamente o tempo que leva para descobrir uma referência a uma imagem, fazer o download dela, decodificá-la e apresentá-la. Vejamos esta marcação de imagem, por exemplo:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
O uso de um prefixo data-
é um padrão comum em carregadores lentos com tecnologia JavaScript. Quando a imagem é rolada até a janela de visualização, o carregador lento remove o prefixo data-
, o que significa que, no exemplo anterior, data-src
se torna src
. Essa atualização solicita que o navegador busque o recurso.
Esse padrão não é problemático até ser aplicado a imagens que estão na janela de visualização durante a inicialização. Como o scanner de pré-carregamento não lê o atributo data-src
da mesma forma que faria com um atributo src
(ou srcset
), a referência da imagem não é descoberta antes. Para piorar, o carregamento da imagem é atrasado até depois do download, da compilação e da execução do JavaScript do carregador lento.
Dependendo do tamanho da imagem, que pode depender do tamanho da janela de visualização, ela pode ser um elemento candidato para a Maior exibição de conteúdo (LCP). Quando o scanner de pré-carregamento não consegue buscar especulativamente o recurso de imagem com antecedência, possivelmente durante o ponto em que as folhas de estilo da página bloqueiam a renderização, a LCP apresenta problemas.
A solução é mudar a marcação de imagem:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Esse é o padrão ideal para imagens que estão na janela de visualização durante a inicialização, já que o scanner de pré-carregamento descobre e busca o recurso de imagem mais rapidamente.
O resultado neste exemplo simplificado é uma melhoria de 100 milissegundos na LCP em uma conexão lenta. Isso pode não parecer uma grande melhoria, mas é quando você considera que a solução é uma correção rápida de marcação e que a maioria das páginas da Web é mais complexa do que esse conjunto de exemplos. Isso significa que os candidatos à LCP podem ter que lidar com a largura de banda com muitos outros recursos, de modo que otimizações como essa se tornam cada vez mais importantes.
Imagens de plano de fundo CSS
O scanner de pré-carregamento do navegador verifica a marcação. Ela não verifica outros tipos de recursos, como CSS, que podem envolver buscas de imagens referenciadas pela propriedade background-image
.
Assim como o HTML, os navegadores processam o CSS em seu próprio modelo de objeto, conhecido como CSSOM. Se os recursos externos forem descobertos durante a construção do CSSOM, eles serão solicitados no momento da descoberta, e não pelo scanner de pré-carregamento.
Digamos que o Candidato à LCP da sua página seja um elemento com uma propriedade background-image
de CSS. Veja o que acontece enquanto os recursos são carregados:
Nesse caso, o scanner de pré-carregamento não é tão derrotado quanto não está envolvido. Mesmo assim, se um candidato a LCP na página for de uma propriedade CSS do background-image
, pré-carregue essa imagem:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
Essa dica rel=preload
é pequena, mas ajuda o navegador a descobrir a imagem mais cedo do que faria:
Com a dica rel=preload
, o candidato à LCP é descoberto mais rapidamente, reduzindo o tempo da LCP. Embora essa dica ajude a corrigir o problema, a melhor opção é avaliar se a imagem candidata à LCP precisa ser carregada do CSS. Com uma tag <img>
, você tem mais controle sobre o carregamento de uma imagem adequada para a janela de visualização, além de permitir que o scanner de pré-carregamento a detecte.
Inserção de muitos recursos em linha
A inserção in-line é uma prática que coloca um recurso dentro do HTML. É possível incorporar folhas de estilo em elementos <style>
, scripts em elementos <script>
e praticamente qualquer outro recurso usando a codificação em base64.
A inserção de recursos pode ser mais rápida do que fazer o download deles porque uma solicitação separada não é emitida para o recurso. Está diretamente no documento e carrega instantaneamente. No entanto, existem desvantagens significativas:
- Se você não estiver armazenando seu HTML em cache (e simplesmente não o consegue se a resposta HTML for dinâmica), os recursos embutidos nunca serão armazenados em cache. Isso afeta o desempenho porque os recursos embutidos não são reutilizáveis.
- Mesmo que você possa armazenar HTML em cache, os recursos inline não são compartilhados entre documentos. Isso reduz a eficiência do armazenamento em cache em comparação com arquivos externos que podem ser armazenados em cache e reutilizados em uma origem inteira.
- Se você inserir muito conteúdo inline, atrasará o scanner de pré-carregamento para descobrir recursos posteriormente no documento, porque o download desse conteúdo extra e inline demora mais.
Confira esta página como exemplo. Em determinadas condições, o candidato à LCP é a imagem na parte superior da página, e o CSS está em um arquivo separado carregado por um elemento <link>
. A página também usa quatro fontes da Web solicitadas como arquivos separados do recurso CSS.
O que acontece se o CSS e todas as fontes estiverem em linha como recursos base64?
O impacto do uso de inline traz consequências negativas para a LCP neste exemplo e para o desempenho em geral. A versão da página sem nada inline mostra a imagem da LCP em cerca de 3,5 segundos. A página com tudo inline não pinta a imagem LCP até pouco mais de sete segundos.
Há mais em jogo aqui do que apenas o scanner de pré-carregamento. Fontes em linha não são uma boa estratégia porque base64 é um formato ineficiente para recursos binários. Outro fator em questão é que o download de recursos de fontes externas não é feito, a menos que sejam determinados pelo CSSOM. Quando essas fontes são inline como base64, elas são transferidas por download mesmo que não sejam necessárias para a página atual.
Um pré-carregamento poderia melhorar as coisas? Claro! É possível pré-carregar a imagem da LCP e reduzir o tempo de LCP, mas aumentar o HTML possivelmente não armazenável em cache com recursos inline tem outras consequências negativas de desempenho. A Primeira exibição de conteúdo (FCP, na sigla em inglês) também é afetada por esse padrão. Na versão da página em que nada está inline, a FCP é de aproximadamente 2,7 segundos. Na versão em que tudo está embutido, a FCP tem aproximadamente 5,8 segundos.
Tenha muito cuidado ao inserir itens em linha no HTML, especialmente recursos codificados em base64. Em geral, ela não é recomendada, exceto para recursos muito pequenos. O mínimo possível de alinhamento, porque muito conteúdo in-line está brincando com fogo.
Renderização da marcação com JavaScript do lado do cliente
Com certeza: o JavaScript afeta a velocidade da página. Além de os desenvolvedores dependerem dela para oferecer interatividade, há uma tendência de confiar nela para entregar conteúdo por conta própria. Em alguns aspectos, isso melhora a experiência do desenvolvedor, mas as vantagens para os desenvolvedores nem sempre são benéficas para os usuários.
Um padrão que pode invalidar o scanner de pré-carregamento é renderizar a marcação com JavaScript do lado do cliente:
Quando payloads de marcação são contidos e renderizados inteiramente por JavaScript no navegador, todos os recursos nessa marcação são efetivamente invisíveis para o verificador de pré-carregamento. Isso atrasa a descoberta de recursos importantes, o que certamente afeta a LCP. Nesses exemplos, a solicitação da imagem LCP tem um atraso significativo em comparação com a experiência renderizada pelo servidor equivalente, que não exige que o JavaScript apareça.
Isso se afasta um pouco do foco deste artigo, mas os efeitos da marcação de renderização no cliente vão muito além de invalidar o scanner de pré-carregamento. Por um lado, a introdução do JavaScript para potencializar uma experiência que não exige isso introduz um tempo de processamento desnecessário que pode afetar a Interação com a próxima exibição (INP, na sigla em inglês).
Além disso, renderizar quantidades muito grandes de marcação no cliente tem maior probabilidade de gerar tarefas longas em comparação com a mesma quantidade de marcação enviada pelo servidor. O motivo disso, além do processamento extra que o JavaScript envolve, é que os navegadores fazem streaming da marcação do servidor e dividem a renderização para evitar tarefas longas. A marcação renderizada pelo cliente, por outro lado, é tratada como uma única tarefa monolítica, que pode afetar as métricas de capacidade de resposta da página, como Tempo total de bloqueio (TBT, na sigla em inglês) ou Latência na primeira entrada (FID, na sigla em inglês), além do INP.
A solução para esse cenário depende da resposta a esta pergunta: Existe algum motivo para a marcação da sua página não ser fornecida pelo servidor e não ser renderizada no cliente? Se a resposta for "não", considere a renderização do lado do servidor (SSR) ou a marcação gerada estaticamente sempre que possível, já que isso ajudará o scanner de pré-carregamento a descobrir e buscar recursos importantes antecipadamente de maneira oportuna.
Se sua página precisa de JavaScript para anexar funcionalidades a algumas partes da marcação, você ainda pode usar a SSR, seja com JavaScript comum ou com hidratação, para ter o melhor dos dois mundos.
Ajude o scanner de pré-carregamento a ajudar você
O scanner de pré-carregamento é uma otimização de navegador altamente eficaz que ajuda as páginas a carregarem mais rápido durante a inicialização. Ao evitar padrões que atrapalham a descoberta de recursos importantes com antecedência, você não está apenas simplificando o desenvolvimento, mas também cria experiências melhores para os usuários que geram resultados melhores em muitas métricas, incluindo algumas Web Vitals.
Para recapitular, veja os pontos a seguir que convém tirar desta postagem:
- O scanner de pré-carregamento do navegador é um analisador de HTML secundário que verifica antes do principal se estiver bloqueado para descobrir recursos oportunistas que pode ser buscado antes.
- Recursos que não estão presentes na marcação fornecida pelo servidor na solicitação de navegação inicial não podem ser descobertos pelo scanner de pré-carregamento. As maneiras de invalidar o scanner de pré-carregamento podem incluir, entre outras:
- Injetar recursos no DOM com JavaScript, sejam scripts, imagens, folhas de estilo ou qualquer outra coisa que seria melhor no payload de marcação inicial do servidor.
- Carregamento lento de imagens ou iframes acima da dobra usando uma solução JavaScript.
- Marcação de renderização no cliente que pode conter referências a sub-recursos do documento usando JavaScript.
- O scanner de pré-carregamento verifica somente HTML. Ela não examina o conteúdo de outros recursos, especialmente CSS, que podem incluir referências a recursos importantes, incluindo candidatos à LCP.
Se, por qualquer motivo, não for possível evitar um padrão que afete negativamente a capacidade do scanner de pré-carregamento de acelerar o desempenho de carregamento, considere a dica de recurso rel=preload
. Se você usa rel=preload
, teste nas ferramentas do laboratório para garantir que está gerando o efeito desejado. Por fim, não pré-carregue muitos recursos, porque quando você priorizar tudo, nada será feito.
Recursos
- Scripts assíncronos injetados com script são considerados nocivos
- Como o pré-carregador do navegador acelera o carregamento das páginas
- Pré-carregue recursos críticos para melhorar a velocidade de carregamento
- Estabeleça conexões de rede cedo para melhorar a velocidade percebida da página
- Como otimizar a Maior exibição de conteúdo
Imagem principal do Unsplash, de Mohammad Rahmani .