Identificar e resolver gargalos de desempenho no caminho crítico de renderização exige um bom conhecimento dos problemas comuns. Vamos fazer um tour prático e extrair padrões de desempenho comuns que ajudarão você a otimizar suas páginas.
A otimização do caminho crítico de renderização permite que o navegador pinte a página o mais rápido possível. Páginas mais rápidas resultam em maior engajamento, mais visualizações e melhoria nas conversões. Para minimizar o tempo que um visitante passa visualizando uma tela em branco, precisamos otimizar quais recursos são carregados e em que ordem.
Para ajudar a ilustrar esse processo, vamos começar com o caso mais simples possível e construir nossa página de forma incremental para incluir recursos, estilos e lógica de aplicativo adicionais. No processo, otimizaremos cada caso, e vamos descobrir onde algo pode dar errado.
Até agora, nos concentramos exclusivamente no que acontece no navegador depois que o recurso (arquivo CSS, JS ou HTML) fica disponível para processamento. Ignoramos o tempo necessário para buscar o recurso no cache ou na rede. Presumimos o seguinte:
- A ida e volta da rede (latência de propagação) até o servidor leva 100 ms.
- O tempo de resposta do servidor é de 100 ms para documentos HTML e 10 ms para todos os outros arquivos.
A experiência "Hello World"
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
Vamos começar com uma marcação HTML básica e uma única imagem, sem CSS ou JavaScript. Vamos abrir nossa linha do tempo de rede no Chrome DevTools e inspecionar a hierarquia de recursos resultante:
Como esperado, o download do arquivo HTML levou cerca de 200 ms. A parte transparente da linha azul representa o tempo que o navegador espera na rede sem receber bytes de resposta, enquanto a parte sólida mostra o tempo para concluir o download após o recebimento dos primeiros bytes de resposta. O download do HTML é muito pequeno (menos de 4K), então só precisamos de uma única ida e volta para buscar o arquivo completo. Como resultado, o documento HTML leva aproximadamente 200 ms para ser buscado, com metade do tempo gasto aguardando a rede e a outra metade aguardando a resposta do servidor.
Quando o conteúdo HTML é disponibilizado, o navegador analisa os bytes, converte-os em tokens e cria a árvore do DOM. Observe que o DevTools informa o tempo do evento DOMContentLoaded na parte inferior (216 ms), o que também corresponde à linha vertical azul. A lacuna entre o final do download do HTML e a linha vertical azul (DOMContentLoaded) é o tempo que o navegador leva para criar a árvore do DOM — nesse caso, apenas alguns milissegundos.
Nossa "foto incrível" não bloqueou o evento domContentLoaded
. Acontece que é possível construir a árvore de renderização e até pintar a página sem esperar cada recurso na página: nem todos os recursos são essenciais para entregar a primeira exibição rápida. De fato, quando falamos sobre o caminho crítico de renderização, normalmente estamos falando sobre marcação HTML, CSS e JavaScript. As imagens não bloqueiam a renderização inicial da página, embora também devíamos tentar pintar as imagens o mais rápido possível.
Sendo assim, o evento load
(também conhecido como onload
) está bloqueado na imagem: o DevTools informa o evento onload
aos 335 ms. Lembre-se de que o evento onload
marca o ponto em que todos os recursos necessários para a página foram transferidos por download e processados. Nesse ponto, o ícone de carregamento pode parar de girar no navegador (a linha vertical vermelha na hierarquia).
Como adicionar JavaScript e CSS à combinação
Nossa página "Hello World" parece simples, mas acontece muito nos bastidores. Na prática, precisaremos de mais do que apenas o HTML. É provável que haja uma folha de estilo CSS e um ou mais scripts para adicionar interatividade à página. Vamos adicionar ambos à nossa solução e ver o que acontece:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Script</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="timing.js"></script>
</body>
</html>
Antes de adicionar JavaScript e CSS:
Com JavaScript e CSS:
Adicionar arquivos CSS e JavaScript externos inclui duas solicitações extras à cascata, que são enviadas pelo navegador quase ao mesmo tempo. No entanto, é importante notar que agora a diferença de tempo entre os eventos domContentLoaded
e onload
é muito menor.
o que aconteceu?
- Ao contrário do nosso exemplo de HTML simples, também precisamos buscar e analisar o arquivo CSS para construir o CSSOM. Precisamos do DOM e do CSSOM para criar a árvore de renderização.
- Como a página também contém um arquivo JavaScript que bloqueia o analisador, o evento
domContentLoaded
é bloqueado até que o arquivo CSS seja transferido por download e analisado. Como o JavaScript pode consultar o CSSOM, precisamos bloquear o arquivo CSS até que ele seja transferido por download antes de executar o JavaScript.
E se substituirmos nosso script externo por um script in-line? Mesmo que o script esteja embutido diretamente na página, o navegador não poderá executá-lo até que o CSSOM seja criado. Em resumo, o JavaScript em linha também bloqueia o analisador.
Dito isso, apesar do bloqueio no CSS, a inserção in-line do script faz com que a página seja renderizada mais rapidamente? Vamos testar e ver o que acontece.
JavaScript externo:
JavaScript inline:
Estamos fazendo uma solicitação a menos, mas os tempos de onload
e domContentLoaded
são efetivamente os mesmos. Por quê? Bem, sabemos que não importa se o JavaScript está em linha ou externo, pois assim que o navegador chega à tag script, ele bloqueia e aguarda a criação do CSSOM. Além disso, em nosso primeiro exemplo, o navegador faz o download de CSS e JavaScript em paralelo, e o download é concluído aproximadamente ao mesmo tempo. Nesse caso, a inserção em linha do código JavaScript não nos ajuda muito. No entanto, há várias estratégias que podem fazer com que a página seja renderizada mais rapidamente.
Primeiro, lembre-se de que todos os scripts inline bloqueiam o analisador, mas podemos adicionar a palavra-chave "async" aos scripts externos para desbloquear o analisador. Vamos desfazer a inserção e fazer um teste:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Async</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script async src="timing.js"></script>
</body>
</html>
JavaScript com bloqueio de analisador (externo):
JavaScript assíncrono (externo):
Muito melhor! O evento domContentLoaded
é disparado logo após a análise do HTML. O navegador sabe que não precisa bloquear no JavaScript e, como não há outros scripts de bloqueio de analisador, a construção do CSSOM também pode continuar em paralelo.
Como alternativa, poderíamos ter inline tanto o CSS quanto o JavaScript:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Inlined</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
</style>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
O tempo de domContentLoaded
é efetivamente o mesmo do exemplo anterior. Em vez de marcar nosso JavaScript como assíncrono, colocamos o CSS e o JS em linha na própria página. Isso torna nossa página HTML muito maior, mas a vantagem é que o navegador não precisa esperar para buscar recursos externos; está tudo bem ali na página.
Como você pode ver, mesmo com uma página muito simples, otimizar o caminho crítico de renderização é um exercício não trivial: precisamos entender o gráfico de dependências entre recursos diferentes, identificar quais recursos são "críticos" e escolher entre diferentes estratégias para incluir esses recursos na página. Não há uma solução única para esse problema, cada página é diferente. Você precisa seguir um processo semelhante por conta própria para descobrir a estratégia ideal.
Dito isso, vamos tentar identificar alguns padrões gerais de desempenho.
Padrões de desempenho
A página mais simples possível consiste apenas na marcação HTML. Não use CSS, JavaScript ou outros tipos de recursos. Para renderizar essa página, o navegador precisa iniciar a solicitação, aguardar a chegada do documento HTML, analisá-lo, criar o DOM e, por fim, renderizá-lo na tela:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
O tempo entre T0 e T1 captura os tempos de processamento da rede e do servidor. Na melhor das hipóteses (se o arquivo HTML for pequeno), basta uma ida e volta na rede para buscar todo o documento. Devido à forma como os protocolos de transporte TCP funcionam, arquivos maiores podem exigir mais idas e voltas. Como resultado, no melhor caso, a página acima tem um caminho crítico de renderização (no mínimo) de uma ida e volta.
Agora, vamos considerar a mesma página, mas com um arquivo CSS externo:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
Mais uma vez, invocamos uma ida e volta na rede para buscar o documento HTML, e a marcação recuperada nos diz que também precisamos do arquivo CSS. Isso significa que o navegador tem que voltar ao servidor e obter o CSS antes de renderizar a página na tela. Como resultado, a página precisa de, no mínimo, duas idas e voltas antes de ser exibida. Mais uma vez, o arquivo CSS pode exigir várias idas e voltas, por isso a ênfase em "mínimo".
Vamos definir o vocabulário que usamos para descrever o caminho crítico de renderização:
- Recurso crítico:recurso que pode bloquear a renderização inicial da página.
- Tamanho do caminho crítico:número de idas e voltas ou o tempo total necessário para buscar todos os recursos críticos.
- Bytes críticos: número total de bytes necessários para chegar à primeira renderização da página, que é a soma dos tamanhos dos arquivos de transferência de todos os recursos críticos. Nosso primeiro exemplo, com uma única página HTML, continha um único recurso crítico (o documento HTML); o tamanho do caminho crítico também era igual a uma ida e volta da rede (supondo que o arquivo fosse pequeno), e o total de bytes críticos era apenas o tamanho da transferência do próprio documento HTML.
Agora, vamos comparar isso com as características de caminho crítico do exemplo HTML + CSS acima:
- 2 recursos críticos
- 2 ou mais idas e voltas para o tamanho mínimo do caminho crítico
- 9 KB de bytes críticos
Precisamos do HTML e do CSS para construir a árvore de renderização. Como resultado, o HTML e o CSS são recursos críticos: o CSS é buscado somente depois que o navegador recebe o documento HTML. Portanto, o tamanho do caminho crítico é de, no mínimo, duas idas e voltas. Os dois recursos somam um total de 9 KB de bytes críticos.
Agora vamos adicionar mais um arquivo JavaScript à combinação.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
Adicionamos app.js
, que é um recurso externo de JavaScript na página e um recurso de bloqueio de analisador (crítico). Além disso, para executar o arquivo JavaScript, é preciso bloquear e aguardar o CSSOM. Lembre-se de que o JavaScript pode consultar o CSSOM e, portanto, o navegador pausa até que o style.css
seja transferido por download e o CSSOM seja criado.
Dito isso, na prática, se analisarmos a "hierarquia de rede" desta página, você verá que as solicitações de CSS e JavaScript são iniciadas quase ao mesmo tempo. O navegador recebe o HTML, descobre os dois recursos e inicia as duas solicitações. Como resultado, a página acima tem as seguintes características de caminho crítico:
- 3 recursos críticos
- 2 ou mais idas e voltas para o tamanho mínimo do caminho crítico
- 11 KB de bytes críticos
Agora, temos três recursos críticos que totalizam 11 KB de bytes críticos, mas o tamanho do nosso caminho crítico ainda é de duas idas e voltas, pois é possível transferir o CSS e o JavaScript em paralelo. Descobrir as características do caminho crítico de renderização significa identificar os recursos essenciais e entender como o navegador programará as buscas. Vamos continuar com nosso exemplo.
Depois de conversar com os desenvolvedores do site, percebemos que o JavaScript incluído na página não precisa bloquear. Temos algumas análises e outros códigos que não precisam bloquear a renderização da página. Sabendo disso, podemos adicionar o atributo "async" à tag script para desbloquear o analisador:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
Um script assíncrono tem várias vantagens:
- O script não bloqueia mais o analisador e não faz parte do caminho crítico de renderização.
- Como não há outros scripts críticos, o CSS não precisa bloquear o evento
domContentLoaded
. - Quanto mais cedo o evento
domContentLoaded
for acionado, mais cedo será possível executar outra lógica do aplicativo.
Como resultado, nossa página otimizada agora voltou a ter dois recursos críticos (HTML e CSS), com um tamanho mínimo de caminho crítico de duas idas e voltas e um total de 9 KB de bytes críticos.
Por fim, se a folha de estilo CSS só fosse necessária para impressão, como ficaria isso?
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" media="print" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
Como o recurso style.css é usado apenas para impressão, o navegador não precisa de um bloqueio para renderizar a página. Portanto, assim que a construção do DOM for concluída, o navegador terá informações suficientes para renderizar a página. Como resultado, essa página tem apenas um único recurso crítico (o documento HTML), e o tamanho mínimo do caminho crítico de renderização é uma ida e volta.