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

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 construa a nossa página de forma incremental 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 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

Comece com uma marcação HTML básica e uma única imagem, sem CSS e JavaScript. Em seguida, abra o painel "Network" no Chrome DevTools e inspecione a cascata 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 nenhum byte de resposta, enquanto a parte sólida mostra o tempo decorrido para a finalização do download depois do recebimento do primeiro byte de resposta. O download do HTML é bem pequeno (menos de 4 KB), então, só precisamos de uma única ida e volta para termos o arquivo inteiro. Como resultado, o documento HTML é obtido em cerca de 200 ms, com metade do tempo gasto ficando a cargo da espera da rede e a outra metade, à espera da 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. O que acontece é que podemos construir a árvore de renderização e até aplicar cor à página sem aguardar todos os ativos serem carregados: nem todos os recursos são essenciais para fornecer a primeira pintura rapidamente. Na verdade, quando falamos do 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. O evento onload marca o ponto em que todos os recursos necessários para a página foram baixados e processados. Nesse momento, o ícone de carregamento pode parar de girar no navegador (a linha vertical vermelha na cascata).

Adicionar JavaScript e CSS à página

Nossa página "Experiência Hello World" parece básica, mas, por trás dos panos, não é bem assim. 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>

Faça um teste

Antes de adicionar JavaScript e CSS:

CRP do DOM

Com JavaScript e CSS:

DOM, CSSOM e JS

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 o script externo com um script inline? 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:

DOM, CSSOM e JS

JavaScript inline:

DOM, CSSOM e JS 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. Mas existem várias outras estratégias que podem fazer a página renderizar mais rápido.

Primeiro, lembre-se de que todos os scripts inline bloqueiam o analisador, mas podemos adicionar o atributo async aos scripts externos para desbloqueá-lo. 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>

Faça um teste

JavaScript com bloqueio do analisador (externo):

DOM, CSSOM e JS

JavaScript assíncrono (externo):

DOM, CSSOM e JS assíncrono

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>

Faça um teste

DOM, CSS inline e JS inline

O tempo de domContentLoaded é igual ao do exemplo anterior. Em vez de marcar o JavaScript como assíncrono, colocamos o CSS e o JS em linha na própria página. Isso aumentou muito o tamanho da nossa página HTML, mas trouxe um ponto positivo: o navegador não precisa esperar para buscar recursos externos porque já está tudo 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ência entre diferentes recursos, 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.

Considerando tudo isso, vamos tentar voltar um pouco 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 deve inicializar a solicitação, aguardar a chegada do documento HTML, analisá-lo, criar o DOM e finalmente renderizar o documento 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

Hello world CRP

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, arquivos maiores podem exigir 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>

Faça um teste

DOM + CSSOM CRP

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 "mínimo".

Estes sã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 a 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:

DOM + CSSOM CRP

  • 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. Por isso, ambos são recursos críticos: o CSS é buscado somente depois que o navegador obtém o documento HTML. Sendo assim, o tamanho do caminho crítico é, no mínimo, duas idas e voltas. Ambos os recursos representam um total de 9 KB de bytes críticos.

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>

Faça um teste

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, temos que parar e esperar o CSSOM. Lembre-se de que o JavaScript pode consultar o CSSOM e, por isso, o navegador para até que style.css seja baixado e o CSSOM seja criado.

DOM, CSSOM e JavaScript CRP

No entanto, na prática, se observarmos a "cascata de rede" dessa página, notaremos que as solicitações CSS e JavaScript são iniciadas praticamente ao mesmo tempo. O navegador obtém o HTML, descobre 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 totalizam 11 KB de bytes críticos. No entanto, o comprimento do nosso caminho crítico continua sendo duas idas e voltas, já que podemos transferir o CSS e o JavaScript em paralelo. Conhecer as características do seu caminho crítico de renderização significa poder identificar os recursos vitais e também entender como o navegador agenda a busca deles.

Depois de conversar com os desenvolvedores do nosso site, percebemos que o JavaScript que incluímos na página não precisa bloquear o processo. Temos algumas análises e código que dispensam a necessidade de 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>

Faça um teste

DOM, CSSOM e JavaScript assíncrono CRP

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

Faça um teste

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

Como o recurso style.css só é usado para impressão, o navegador não precisa parar nele 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.

Feedback