Publicado em 31 de dezembro de 2013
O JavaScript nos permite modificar quase todos os aspectos da página: conteúdo, estilo e resposta à interação do usuário. No entanto, o JavaScript também pode bloquear a construção do DOM e atrasar a renderização da página. Para proporcionar um desempenho ideal, faça seu JavaScript assíncrono e elimine qualquer JavaScript desnecessário do caminho crítico de renderização.
Resumo
- O JavaScript pode consultar e modificar o DOM e o CSSOM.
- A execução do JavaScript bloqueia o CSSOM.
- O JavaScript bloqueia a construção do DOM, a menos que declarado explicitamente como assíncrono.
O JavaScript é uma linguagem dinâmica executada em um navegador que permite alterar praticamente todos os aspectos do comportamento da página. Podemos modificar o conteúdo adicionando e removendo elementos da árvore do DOM; podemos modificar as propriedades do CSSOM de cada elemento; podemos lidar com as interações do usuário; entre muitas outras funções. Para ilustrar isso, confira o que acontece quando o exemplo anterior "Hello World" é alterado para adicionar um script em linha curto:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script</title>
</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 JavaScript permite acessar o DOM e extrair a referência ao nó de span oculto. O nó pode não estar visível na árvore de renderização, mas está presente no DOM. Depois, quando tivermos a referência, podemos mudar o texto (usando .textContent) e até modificar a propriedade calculada de estilo de exibição de "none" para "inline". Agora nossa página mostra "Hello Interactive student!" (Olá, estudantes interativos).
O JavaScript também permite criar, aplicar estilos, anexar e remover novos elementos no DOM. Tecnicamente, a nossa página inteira poderia ser apenas um grande arquivo de JavaScript que cria e aplica estilos aos elementos, um por um. Embora isso funcione, na prática, usar HTML e CSS é muito mais fácil. Na segunda parte da nossa função JavaScript, criamos um novo elemento div, definimos o conteúdo de texto, aplicamos um estilo e o anexamos ao corpo.
Com isso, modificamos o conteúdo e o estilo CSS de um nó DOM existente e adicionamos um nó totalmente novo ao documento. Nossa página não receberá nenhum prêmio de design, mas mostra o poder e a flexibilidade que o JavaScript nos oferece.
Porém, embora o JavaScript nos dê muito poder, ele cria muitas limitações adicionais sobre como e quando a página é renderizada.
Primeiro, observe no exemplo anterior que o script inline está perto do final da página. Por quê? Bem, você dever tentar fazer isso, mas se mudarmos o script para acima do elemento <span>
, ele falha e informa que não consegue encontrar uma referência para qualquer elemento <span>
no documento. Ou seja, getElementsByTagName('span')
retorna null
. Isso demonstra uma propriedade importante: o script é executado no ponto exato em que é inserido no documento. Quando o analisador HTML encontra uma tag script, ele pausa o processo de construção do DOM e passa o controle para o mecanismo do JavaScript. Depois que o mecanismo do JavaScript conclui a execução, o navegador retoma de onde parou e retoma a construção do DOM.
Em outras palavras, nosso bloco de script não consegue encontrar elementos definidos posteriormente na página porque ainda não foram processados. Ou, de forma ligeiramente diferente, a execução do nosso script inline bloqueia a construção do DOM, o que, por sua vez, também retarda a renderização inicial.
Outra propriedade sutil da inclusão de scripts em nossa página é que, além de ler e modificar o DOM, eles também podem fazer o mesmo nas propriedades do CSSOM. Na verdade, é exatamente isso que estamos fazendo em nosso exemplo quando alteramos a propriedade de exibição do elemento span de nenhum para inline. O resultado final? Agora temos uma disputa.
E se o navegador não tiver concluído o download e a criação do CSSOM no momento da execução do script? A resposta não é muito boa para a performance: o navegador interrompe a execução do script e a construção do DOM até concluir o download e a construção do CSSOM.
Em suma, o JavaScript introduz uma série de novas dependências entre o DOM, o CSSOM e a execução de JavaScript. Isso pode causar atrasos significativos no navegador ao processar e renderizar a página na tela:
- O local do script no documento é significativo.
- Quando o navegador encontra uma tag script, a construção do DOM é pausada até que a execução do script seja concluída.
- O JavaScript pode consultar e modificar o DOM e o CSSOM.
- A execução do JavaScript é pausada até que o CSSOM esteja pronto.
Em grande medida, "otimizar o caminho crítico de renderização" se refere a compreender e otimizar o gráfico de dependência entre HTML, CSS e JavaScript.
Bloqueio de analisador versus JavaScript assíncrono
Por padrão, a execução do JavaScript bloqueia o analisador. Quando o navegador encontra um script no documento, deve suspender a construção do DOM, passar o controle ao tempo de execução do JavaScript e deixar que o script seja executado antes de continuar com a construção do DOM. Vimos como isso funciona com um script em linha no exemplo anterior. Na verdade, scripts inline sempre bloqueiam o analisador, a menos que você crie código adicional para adiar a execução dos scripts.
E os scripts incluídos com uma tag de script? Pegue o exemplo anterior e extraia o código em um arquivo separado:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script External</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
app.js
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);
Se usarmos uma tag <script> ou um snippet JavaScript in-line, é esperado que ambos se comportem da mesma maneira. Em ambos os casos, o navegador pausa e executa o script antes de processar o restante do documento. No entanto, no caso de um arquivo JavaScript externo, o navegador precisa pausar para esperar que o script seja buscado do disco, do cache ou de um servidor remoto, o que pode adicionar dezenas a milhares de milissegundos de atraso ao caminho crítico de renderização.
Por padrão, todo JavaScript bloqueia o analisador. Como o navegador não sabe o que o script planeja fazer na página, ele assume o pior cenário e bloqueia o analisador. Um sinal para o navegador de que o script não precisa ser executado no ponto exato onde é referenciado permite que o navegador continue construindo o DOM e deixe o script ser executado quando estiver pronto; por exemplo, depois que o arquivo é recuperado a partir do cache ou de um servidor remoto.
Para fazer isso, o atributo async
é adicionado ao elemento <script>
:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script Async</title>
</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>
Adicionar a palavra-chave "async" à tag de script informa ao navegador para não bloquear a construção do DOM enquanto aguarda que o script seja disponibilizado, o que pode melhorar significativamente o desempenho.