Publicado em 31 de março de 2014
Para identificar e resolver gargalos de desempenho no caminho crítico de renderização, é preciso ter bom nível de conhecimento sobre os obstáculos mais comuns. Uma visita guiada para identificar padrões de desempenho comuns vai ajudar você a otimizar suas páginas.
Ao otimizar o caminho crítico de renderização, o navegador pode colorir a página com a maior velocidade possível. Páginas mais rápidas geram maior envolvimento, mais visualizações de páginas e melhores taxas de conversão. Para minimizar o tempo que um visitante perde olhando para uma tela em branco, precisamos definir os recursos a serem carregados e a ordem deles da maneira mais eficaz possível.
Para ajudar a ilustrar esse processo, comece com o caso mais simples possível e aumente a página gradualmente para incluir outros recursos, estilos e lógica de aplicativo. No processo, otimizaremos todos os casos, e também vamos destacar os pontos em que podem acontecer erros.
Até aqui, trabalhamos exclusivamente com o 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, esteja ele armazenado em cache ou na rede. Vamos supor o seguinte:
- Uma ida e volta na 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>
Comece com uma marcação HTML básica e uma única imagem. Sem CSS ou JavaScript. Em seguida, abra o painel "Network" no Chrome DevTools e inspecione a cascata 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 depois que os primeiros bytes de resposta foram recebidos. O download do HTML é pequeno (<4K), então só precisamos de uma única ida e volta para buscar o arquivo completo. Como resultado, a busca do documento HTML leva aproximadamente 200 ms, 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, os converte em tokens e cria a árvore DOM. Observe que o DevTools informa o tempo do evento DOMContentLoaded na parte de baixo (216 ms), que também corresponde à linha vertical azul. O intervalo entre o final do download do HTML e a linha vertical azul (DOMContentLoaded) é o tempo necessário para que o navegador crie a árvore do DOM. Neste caso, apenas alguns milissegundos.
Observe que a nossa "foto incrível" não bloqueou o evento domContentLoaded
. Acontece que podemos construir a árvore de renderização e até mesmo pintar a página sem esperar por cada recurso na página: nem todos os recursos são essenciais para fornecer a first paint rápida. Na verdade, quando falamos sobre o caminho crítico de renderização, normalmente estamos falando da marcação HTML, CSS e JavaScript. As imagens não bloqueiam a renderização inicial da página, mas é importante que elas sejam pintadas o mais rápido possível.
Nesse cenário, o evento load
(também conhecido como onload
) é bloqueado na imagem: o DevTools relata o evento onload
aos 335 ms. Lembre-se de que o evento onload
marca o ponto em que todos os recursos exigidos pela página foram baixados e processados. Nesse momento, o ícone de carregamento pode parar de girar no navegador (a linha vertical vermelha na cascata).
Como adicionar JavaScript e CSS à nossa plataforma
Nossa página "Hello World experience" parece básica, mas muita coisa acontece nos bastidores. Na prática, precisamos de mais do que um simples HTML: provavelmente usaremos uma folha de estilo CSS e um ou mais scripts para acrescentar interatividade à página. Adicione os dois para 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:
Ao adicionarmos arquivos CSS e JavaScript externos, estamos acrescentando duas solicitações à cascata, que são enviadas pelo navegador praticamente juntas. No entanto, há uma diferença de tempo muito menor entre os eventos domContentLoaded
e onload
.
O que aconteceu?
- Ao contrário do exemplo com HTML simples, agora também é necessário buscar e analisar o arquivo CSS para criar o CSSOM, e sabemos que 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
fica bloqueado até que o arquivo CSS seja baixado e analisado. Como o JavaScript pode consultar o CSSOM, precisamos bloquear o arquivo CSS até que o download dele seja concluído antes de podermos executar o JavaScript.
E se substituirmos nosso script externo por um in-line? Mesmo que o script esteja embutido diretamente na página, o navegador não consegue executá-lo antes de o CSSOM ser criado. Resumindo, o JavaScript em linha também bloqueia o analisador.
Pensando nisso, apesar do bloqueio do CSS, será que embutir o script acelera a renderização da página? Teste e veja o que acontece.
JavaScript externo:
JavaScript inline:
Estamos fazendo uma solicitação a menos, mas os tempos de onload
e domContentLoaded
são praticamente os mesmos. Por quê? Bem, sabemos que não importa se o JavaScript está embutido ou é externo porque assim que o navegador chegar à tag "script", ele para e aguarda a criação do CSSOM. Além disso, no nosso primeiro exemplo, o navegador baixa CSS e JavaScript em paralelo, e o download leva mais ou menos o mesmo tempo. Nessa instância, embutir o código JavaScript não ajuda muito. No entanto, há várias estratégias que podem fazer a página renderizar mais rapidamente.
Primeiro, lembre-se de que todos os scripts inline bloqueiam o analisador, mas para scripts externos, podemos adicionar o atributo async
para desbloquear o analisador. Desfaça o inline e tente o seguinte:
<!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 do analisador (externo):
JavaScript assíncrono (externo):
Bem melhor! O evento domContentLoaded
é acionado logo depois que o HTML é analisado. O navegador sabe que não deve bloquear no JavaScript e, já que não há outros scripts de bloqueio de analisador, a criação do CSSOM também pode prosseguir em paralelo.
Ainda podemos embutir o CSS e o JavaScript no código:
<!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 da domContentLoaded
é efetivamente o mesmo do exemplo anterior. Em vez de marcar o JavaScript como assíncrono, in-line o CSS e o JS 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; tudo está ali na página.
Como você pode ver, mesmo com uma página muito básica, 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 padrão para esse problema, cada página tem suas particularidades. Você deve aplicar um processo parecido por conta própria para chegar à estratégia ideal.
Dito isso, veja se podemos dar um passo atrás e identificar alguns padrões gerais de desempenho.
Padrões de performance
A página mais simples possível é composta apenas de marcação HTML: não tem CSS, JavaScript nem outro tipo de recurso. Para renderizar essa página, o navegador precisa iniciar a solicitação, aguardar a chegada do documento HTML, analisá-lo, criar o DOM e, em seguida, 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. No melhor cenário (se o arquivo HTML for pequeno), basta uma ida e volta na rede para se obter o documento inteiro. Devido à forma como os protocolos de transporte TCP funcionam, é possível que arquivos maiores exijam mais idas e voltas. Como resultado, no melhor cenário, a página acima tem um caminho crítico de renderização com uma ida e volta (no mínimo).
Agora considere 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>
Novamente, precisamos de uma ida e volta na rede para obter o documento HTML. A marcação recuperada nos diz que precisaremos também do arquivo CSS. Isso significa que o navegador tem que voltar ao servidor e buscar o CSS antes de poder renderizar a página na tela. Como resultado, essa página precisa de, pelo menos, 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 "no mínimo".
Aqui estão alguns termos 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.
- Comprimento do caminho crítico: número de idas e voltas ou o total de tempo 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 de arquivo 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 comprimento do caminho crítico também era igual a uma ida e volta na rede (assumindo 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 compare isso com as características de caminho crítico do exemplo anterior de HTML e CSS:
- 2 recursos críticos
- 2 idas e voltas ou mais como comprimento mínimo do caminho crítico
- 9 KB de bytes críticos
Precisamos tanto do HTML quanto do CSS para criar a árvore de renderização. Como resultado, HTML e CSS são recursos críticos. O CSS só é buscado depois que o navegador recebe o documento HTML. Portanto, o tamanho do caminho crítico é de, no mínimo, duas idas e voltas. Ambos os recursos somam 9 KB de bytes críticos no total.
Agora adicione um arquivo JavaScript extra à receita.
<!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 JavaScript externo na página e um recurso que bloqueia o analisador (ou seja, crítico) ao mesmo tempo. E, para piorar, para executar o arquivo JavaScript, precisamos bloquear e aguardar o CSSOM. Lembre-se de que o JavaScript pode consultar o CSSOM e, portanto, o navegador pausa até que style.css
seja baixado e o CSSOM seja criado.
No entanto, na prática, se analisarmos a "cascata de rede" dessa página, vamos notar que as solicitações CSS e JavaScript são iniciadas mais ou menos ao mesmo tempo. O navegador obtém o HTML, encontra os dois recursos e inicia as duas solicitações. Como resultado, a página mostrada na imagem anterior tem as seguintes características de caminho crítico:
- 3 recursos críticos
- 2 idas e voltas ou mais como comprimento mínimo do caminho crítico
- 11 KB de bytes críticos
Agora temos três recursos críticos que somam 11 KB de bytes críticos, mas o tamanho do nosso caminho crítico ainda é de duas idas e voltas, já que é possível transferir o CSS e o JavaScript em paralelo. Descobrir as características do caminho crítico de renderização permite identificar os recursos críticos e entender como o navegador programa as buscas.
Depois de conversar com os desenvolvedores do nosso site, percebemos que o JavaScript incluído na nossa 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 isso, podemos adicionar o atributo async
ao elemento <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 diversas vantagens:
- O script para de bloquear 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 disparado, mais cedo outra lógica do aplicativo poderá começar a ser executada.
Como resultado, nossa página otimizada agora voltou a ter dois recursos críticos (HTML e CSS), com um comprimento 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ó for necessária para a impressão, como ficaria tudo 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 bloqueio para renderizar a página. Portanto, assim que a criação do DOM for concluída, o navegador terá as informações de que precisa para renderizar a página. Como resultado, essa página tem apenas um único recurso crítico (o documento HTML) e o comprimento mínimo do caminho crítico de renderização é uma ida e volta.