Análise do desempenho do caminho crítico de renderização

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>

Faça um teste

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:

CRP

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>

Faça um teste

Antes de adicionar JavaScript e CSS:

CRP DOM

Com JavaScript e CSS:

DOM, CSSOM, JS

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:

DOM, CSSOM, JS

JavaScript inline:

DOM, CSSOM e JS embutido

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>

Faça um teste

JavaScript com bloqueio de analisador (externo):

DOM, CSSOM, JS

JavaScript assíncrono (externo):

DOM, CSSOM, JS assíncrono

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>

Faça um teste

DOM, CSS inline, JS inline

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>

Faça um teste

CRP do Hello World

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>

Faça um teste

CRP do DOM + CSSOM

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:

CRP do DOM + CSSOM

  • 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>

Faça um teste

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.

CRP do DOM, CSSOM e JavaScript

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>

Faça um teste

CRP DOM, CSSOM e JavaScript assíncrono

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>

Faça um teste

CRP do DOM, CSS sem bloqueio e JavaScript assíncrono

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.

Feedback