Performance client-side — Tarefas do navegador na renderização da página

Este é o primeiro de uma série de posts sobre performance client-side.

A série começa explicando como o navegador exibe uma página na tela e quais problemas podem acontecer durante o processo.

Roadmap

  1. Tarefas do navegador na renderização da página (post atual)
  2. Cálculo e análise de métricas
  3. Otimização do Critical Rendering Path

Aplicações web cresceram em tamanho e poder. Com isso, novos desafios
surgiram. Um deles em especial merece destaque, a performance.

Performance influencia tanto na experiência do usuário quanto nas métricas do negócio ($$$, número de acessos, taxa de rejeição etc), de forma negativa ou positiva. Preocupar-se com ela não é tarefa só de back-end ou DevOps, mas também de front-end.

A primeira tarefa de performance a ser trabalhada no client-side é otimizar o tempo que uma página leva para renderizar conteúdo que importa ao usuário. Para isso, é necessário entender como o navegador exibe uma página na tela.

O processo

O navegador precisa realizar um conjunto de tarefas para exibir conteúdo.
Esse grupo é chamado de Critical Rendering Path e é constituído de cinco operações, Construção do DOM, Construção do CSSOM, Criação do Render Tree, Layout e Paint.

Construção do DOM

A primeira etapa é criar o DOM.

Para isso acontecer, o navegador enviará uma request ao servidor para
buscar o HTML da página. O conteúdo retornado será interpretado pelo navegador para que objetos com propriedades e regras sejam criados.
A partir deles será construída uma árvore com base no relacionamento
entre os itens criados. O resultado disso é o DOM.

Para saber quanto tempo essa tarefa leva, basta analisar o evento Parse HTML na aba Timeline do Chrome DevTools.

Construção do CSSOM

Durante a construção do DOM o navegador pode encontrar uma tag link para um arquivo de estilo. Quando isso acontecer, uma nova requisição é iniciada. Após o arquivo ser baixado, um processo semelhante a construção do DOM começará, mas dessa vez para se criar o CSSOM, a árvore de estilos da página. É importante entender que apenas estilos que sobrepõe os do navegador (User-Agent) estarão nessa árvore. O tempo dessa sobreposição também será considerado no processo.
O tempo gasto nessa tarefa estará registrado no evento Recalculate Style na Timeline.

Criação do Render Tree

Agora que DOM e CSSOM estão criados, o próximo passo é combina-los. Isso é o que acontece no Render Tree.
Seu único porém é considerar apenas elementos visíveis na tela, portanto tags com display: none ou invisíveis por padrão (head ou meta, por exemplo) não estarão presentes.
Na Timeline, esse evento pode ser visto como Render Tree.

Layout

A penúltima etapa do processo é o Layout. Aqui, a posição, largura e altura dos elementos presentes no Render Tree são calculados para que o box model (aquele) da página seja gerado.
Essa etapa também é conhecida como Reflow e pode ser vista em ambas as formas na Timeline.

Paint

Por último, pintar o conteúdo gerado nas etapas anteriores na tela.
Essa é a etapa mais lenta, e também a mais trabalhosa. Também pode ser encontrado na Timeline.

O tempo que o navegador levará para completar o Critical Rendering Path varia de acordo com o tamanho e complexidade dos arquivos processados. Além disso, CSS bloqueante e JavaScript bloqueante podem fazer com que o processo dure ainda mais.

CSS bloqueante

A construção da árvore de CSSOM é tão importante para a página que sua execução é bloqueante. Ou seja, o processo de renderização está suspenso até que a construção do CSSOM termine. O navegador trabalha dessa forma para evitar que o usuário veja conteúdo antes do previsto (FOUC, Flash Of Unstyled Content). Por isso, qualquer estilo de página é bloqueante por padrão.

Porém, existem maneiras de definir se um estilo CSS deve ou não bloquear a renderização da página. Uma delas é usar media-queries para estilos de menor importância ou contextos diferentes (estilos de impressão, por exemplo). Isso não fará com que o navegador descarte o arquivo, mas dará menos prioridade a seu carregamento e execução.

JavaScript bloqueante

JavaScript também é bloqueante para a renderização da página. Quando o
navegador encontrar uma tag script, a construção do DOM é suspensa no ponto em que ela foi encontrada no HTML para que o arquivo seja baixado e executado (ou somente executado, se o código é inline).

Além disso, a execução do script deve começar depois da construção do CSSOM; se acontecer antes, ela será suspensa até que a árvore de estilos esteja criada. Com isso, duas atividades estarão paradas, a construção do DOM e a execução do JavaScript.

A regra também vale aqui: o tempo de carregamento e execução do script é proporcional ao seu tamanho e complexidade. Assim como na construção do CSSOM, a execução de JavaScript também é bloqueante por padrão.

Para que código JavaScript não seja bloqueante, deve-se usar os atributos
async ou defer na tag script. O único problema é que eles não funcionam em
scripts inline no HTML, nesse caso ele sempre será bloqueante.

O atributo async vai carregar e executar o código de forma asíncrona, sem
bloquear nada. Por ter execução asíncrona, é aconselhável não usá-lo em arquivos que são dependência para outros, já que o código pode estar indisponível no momento em que for usado. Já o atributo defer fará com que o código seja executado depois que DOM estiver construído.


Próximos passos

Depois de entender o que o navegador faz para exibir uma página na tela e descobrir o que um simples arquivo de CSS ou JavaScript é capaz de fazer para o tempo de renderização da página, o próximo passo é saber como calcular métricas de performance no client-side. Esse será o tema do próximo post da série.