Análise detalhada de um navegador da Web moderno (parte 3)

Mariko Kosaka

Funcionamento interno de um processo de renderizador

Esta é a parte 3 de 4 da série de blogs sobre o funcionamento dos navegadores. Anteriormente, abordamos a arquitetura de vários processos e o fluxo de navegação. Nesta postagem, veremos o que acontece no processo do renderizador.

O processo do renderizador afeta muitos aspectos do desempenho na Web. Como muita coisa acontece dentro do processo do renderizador, esta postagem é apenas uma visão geral. Se quiser saber mais, a seção "Desempenho" dos Fundamentos da Web tem muitos outros recursos.

Os processos do renderizador lidam com conteúdo da Web

O processo do renderizador é responsável por tudo o que acontece dentro de uma guia. Em um processo do renderizador, a linha de execução principal lida com a maior parte do código enviado ao usuário. Às vezes, partes do seu JavaScript são processadas por linhas de execução de worker, se você usa um web worker ou um service worker. As linhas de execução de composição e rasterização também são executadas dentro de um renderizador para renderizar uma página de maneira eficiente e suave.

A principal função do processo do renderizador é transformar HTML, CSS e JavaScript em uma página da Web com a qual o usuário possa interagir.

Processo do renderizador
Figura 1: processo do renderizador com uma linha de execução principal, linhas de execução de worker, uma linha de execução de compositor e uma linha de execução de varredura dentro

Análise

Construção de um DOM

Quando o processo do renderizador recebe uma mensagem de confirmação para uma navegação e começa a receber dados HTML, a linha de execução principal começa a analisar a string de texto (HTML) e a transformá-la em um Model de Document Object (DOM).

O DOM é a representação interna da página do navegador, além da estrutura de dados e da API com que o desenvolvedor Web interage usando JavaScript.

A análise de um documento HTML em um DOM é definida pelo HTML padrão. Talvez você tenha notado que alimentar HTML em um navegador nunca gera um erro. Por exemplo, a ausência da tag </p> de fechamento é um HTML válido. Marcação incorreta, como Hi! <b>I'm <i>Chrome</b>!</i> (a tag b é fechada antes da tag "i") é tratada como se você tivesse programado Hi! <b>I'm <i>Chrome</i></b><i>!</i>. Isso ocorre porque a especificação HTML foi projetada para lidar com esses erros adequadamente. Se você quiser saber como essas coisas são feitas, leia a seção Uma introdução ao tratamento de erros e casos estranhos no analisador (link em inglês) da especificação HTML.

Carregamento de recursos secundários

Geralmente, um site usa recursos externos, como imagens, CSS e JavaScript. Esses arquivos precisam ser carregados pela rede ou pelo cache. A linha de execução principal poderia solicitá-las uma a uma à medida que as encontrava durante a análise para criar um DOM, mas, para acelerar, o "verificação de pré-carregamento" é executado simultaneamente. Se houver itens como <img> ou <link> no documento HTML, o scanner de pré-carregamento exibe os tokens gerados pelo analisador HTML e envia solicitações para a linha de execução da rede no processo do navegador.

DOM
Figura 2: a linha de execução principal que analisa o HTML e cria uma árvore do DOM

JavaScript pode bloquear a análise

Quando o analisador HTML encontra uma tag <script>, ele pausa a análise do documento HTML e precisa carregar, analisar e executar o código JavaScript. Por que o JavaScript pode mudar a forma do documento usando itens como document.write(), que muda toda a estrutura do DOM. A visão geral do modelo de análise na especificação HTML tem um bom diagrama. É por isso que o analisador HTML precisa aguardar a execução do JavaScript antes de retomar a análise do documento HTML. Se você quiser saber o que acontece na execução do JavaScript, a equipe do V8 tem conversas e postagens do blog sobre isso (link em inglês).

Dica para o navegador sobre como você quer carregar recursos

Os desenvolvedores Web podem enviar dicas para o navegador de várias maneiras para que os recursos sejam carregados corretamente. Se o JavaScript não usa document.write(), adicione o atributo async ou defer à tag <script>. Em seguida, o navegador carrega e executa o código JavaScript de forma assíncrona e não bloqueia a análise. Você também pode usar o módulo JavaScript, se isso for adequado. O <link rel="preload"> é uma forma de informar ao navegador que o recurso é realmente necessário para a navegação atual e que você quer fazer o download o mais rápido possível. Leia mais sobre isso em Priorização de recursos: como o navegador pode ajudar você.

Cálculo do estilo

Ter um DOM não é suficiente para determinar a aparência da página, porque podemos definir o estilo dos elementos no CSS. A linha de execução principal analisa o CSS e determina o estilo calculado para cada nó do DOM. São informações sobre que tipo de estilo é aplicado a cada elemento com base em seletores de CSS. Confira essas informações na seção computed do DevTools.

Estilo calculado
Figura 3: a linha de execução principal que analisa o CSS para adicionar o estilo calculado

Mesmo que você não forneça nenhum CSS, cada nó do DOM tem um estilo computado. A tag <h1> é exibida acima da tag <h2> e as margens são definidas para cada elemento. Isso porque o navegador tem uma folha de estilo padrão. Se você quiser saber como é o CSS padrão do Chrome, consulte o código-fonte aqui.

Layout

Agora, o processo do renderizador sabe a estrutura de um documento e os estilos de cada nó, mas isso não é suficiente para renderizar uma página. Imagine que você está tentando descrever uma pintura para um amigo por telefone. "Há um grande círculo vermelho e um pequeno quadrado azul" não é informação suficiente para que seu amigo saiba exatamente como seria a pintura.

jogo de máquina de fax humano
Figura 4: uma pessoa em frente a uma pintura, com uma linha telefônica conectada à outra pessoa

O layout é um processo para encontrar a geometria dos elementos. A linha de execução principal percorre o DOM e os estilos calculados e cria a árvore de layout, que tem informações como coordenadas x y e tamanhos de caixas delimitadoras. A árvore de layout pode ter uma estrutura semelhante à árvore do DOM, mas contém apenas informações relacionadas ao que é visível na página. Se display: none for aplicado, esse elemento não fará parte da árvore de layout. No entanto, um elemento com visibility: hidden estará na árvore de layout. Da mesma forma, se um pseudoelemento com conteúdo como p::before{content:"Hi!"} for aplicado, ele será incluído na árvore de layout, mesmo que não esteja no DOM.

layout
Figura 5: a linha de execução principal que passa pela árvore DOM com estilos computados e produzindo a árvore de layout
Figura 6: layout de caixa para movimento de parágrafo devido a uma mudança de quebra de linha

Determinar o layout de uma página é uma tarefa desafiadora. Mesmo o layout de página mais simples, como um fluxo de blocos de cima para baixo, precisa considerar o tamanho da fonte e onde quebrar a linha, porque isso afeta o tamanho e a forma de um parágrafo, o que afeta onde o parágrafo a seguir precisa estar.

O CSS pode fazer o elemento flutuar para um lado, mascarar o item flutuante e alterar as direções de escrita. Como você pode imaginar, esse estágio de layout tem uma tarefa poderosa. No Chrome, uma equipe inteira de engenheiros trabalha no layout. Se você quiser ver detalhes do trabalho, algumas palestras da BlinkOn Conference são gravadas e são bastante interessantes de assistir.

Tinta

jogo de desenhar
Figura 7: uma pessoa na frente de uma tela segurando um pincel, pensando se deve desenhar um círculo primeiro ou um quadrado

Ter um DOM, um estilo e um layout ainda não é suficiente para renderizar uma página. Digamos que você esteja tentando reproduzir uma pintura. Você sabe o tamanho, a forma e a localização dos elementos, mas ainda precisa julgar a ordem em que os pinta.

Por exemplo, z-index pode ser definido para determinados elementos. Nesse caso, a pintura na ordem dos elementos gravados no HTML resultará em uma renderização incorreta.

falha do Z-index
Figura 8: elementos da página que aparecem na ordem de uma marcação HTML, resultando em uma imagem renderizada incorreta porque o Z-index não foi considerado

Nesta etapa de pintura, a linha de execução principal percorre a árvore de layout para criar registros de pintura. O registro de pintura é uma nota do processo de pintura, como "primeiro plano de fundo, depois texto e depois retângulo". Se você desenhou no elemento <canvas> usando JavaScript, esse processo pode ser familiar para você.

pintar discos
Figura 9: a linha de execução principal passando pela árvore de layout e produzindo registros de exibição

Atualizar o pipeline de renderização é dispendioso

Figura 10: árvores DOM+Style, Layout e Paint na ordem em que são geradas.

O mais importante a entender no pipeline de renderização é que o resultado da operação anterior é usado em cada etapa para criar novos dados. Por exemplo, se algo mudar na árvore de layout, será necessário gerar novamente a ordem de pintura para as partes afetadas do documento.

Se você estiver animando elementos, o navegador terá que executar essas operações entre cada quadro. A maioria das nossas telas atualiza a tela 60 vezes por segundo (60 QPS). A animação aparecerá suave para os olhos humanos quando você mover os itens pela tela em cada frame. No entanto, se a animação não tiver os frames intermediários, a página aparecerá "instável".

jage instabilidade por frames ausentes
Figura 11: frames de animação em uma linha do tempo

Mesmo que as operações de renderização estejam acompanhando a atualização da tela, esses cálculos são executados na linha de execução principal, o que significa que eles podem ser bloqueados quando o aplicativo está executando o JavaScript.

jage jank por JavaScript
Figura 12: frames de animação em uma linha do tempo, mas um frame é bloqueado pelo JavaScript

É possível dividir a operação de JavaScript em pequenos blocos e programar a execução em cada frame usando requestAnimationFrame(). Para saber mais sobre esse tópico, consulte Otimizar a execução do JavaScript. Também é possível executar o JavaScript no Web Workers para evitar o bloqueio da linha de execução principal.

frame de animação da solicitação
Figura 13: blocos menores de JavaScript em execução em uma linha do tempo com frame de animação

Composição

Como você desenharia uma página?

Figura 14: animação do processo de varredura simples

Agora que o navegador sabe a estrutura do documento, o estilo de cada elemento, a geometria da página e a ordem de pintura, como ele desenha uma página? A transformação dessas informações em pixels na tela é chamada de rasterização.

Talvez uma maneira simples de lidar com isso seja rasterizar partes dentro da janela de visualização. Se um usuário rolar a página, mova o frame rasterizado e preencha as partes que faltam fazendo uma varredura mais. Foi assim que o Chrome lidava com a varredura quando foi lançado. No entanto, os navegadores mais recentes executam um processo mais sofisticado, chamado de composição.

O que é composição

Figura 15: animação do processo de composição

A composição é uma técnica para separar partes de uma página em camadas, fazer a varredura separadamente e compor como uma página em uma linha de execução separada, chamada de linha de execução de composição. Se a rolagem acontecer, como as camadas já foram rasterizadas, basta compor um novo frame. A animação pode ser feita da mesma maneira, movendo camadas e compondo um novo frame.

É possível conferir como seu site é dividido em camadas no DevTools usando o painel Layers.

Divisão em camadas

Para descobrir quais elementos precisam estar em quais camadas, a linha de execução principal percorre a árvore de layout para criá-la. Essa parte é chamada de "Atualizar a árvore de camadas" no painel de desempenho do DevTools. Se determinadas partes de uma página que deveriam ser uma camada separada (como o menu lateral deslizante) não estiverem recebendo uma, você pode indicar ao navegador usando o atributo will-change no CSS.

árvore de camadas
Figura 16: a linha de execução principal que percorre a árvore de camadas que produz a árvore de layout

Pode ser tentador fornecer camadas para cada elemento, mas a composição em um número excessivo de camadas pode resultar em uma operação mais lenta do que a rasterização de partes pequenas de uma página a cada frame. Por isso, é fundamental medir o desempenho da renderização do aplicativo. Para saber mais sobre o assunto, consulte Usar propriedades somente para compositores e gerenciar o número de camadas.

Varredura e composição fora da linha de execução principal

Depois que a árvore de camadas é criada e as ordens de pintura são determinadas, a linha de execução principal confirma essas informações na linha de execução do compositor. Em seguida, o thread do compositor faz a varredura de cada camada. Uma camada pode ser grande, como todo o tamanho de uma página. Portanto, a linha de execução do compositor os divide em blocos e envia cada bloco para linhas de execução rasterizadas. As linhas de execução rasterizadas fazem a varredura de cada bloco e os armazenam na memória da GPU.

rasterização
Figura 17: linhas de execução rasterizadas criando o bitmap de blocos e enviando à GPU

A linha de execução do compositor pode priorizar diferentes linhas de execução de varredura para que os itens na janela de visualização (ou próximos) possam ser rasterizados primeiro. Uma camada também tem vários blocos com diferentes resoluções para lidar com ações como o aumento de zoom.

Depois que os blocos são rasterizados, a linha de execução do compositor coleta informações de blocos chamadas quadrados de desenho para criar um frame do compositor.

Desenhar quadriciclos Contém informações como a localização do bloco na memória e onde ele deve ser desenhado considerando a criação da página.
Frame do compositor Uma coleção de quadriculados de desenho que representa o frame de uma página.

Um frame de compositor é enviado para o processo de navegação via IPC. Nesse momento, outro frame de compositor pode ser adicionado a partir da linha de execução de interface para a mudança da interface do navegador ou de outros processos de renderizador para extensões. Esses frames do compositor são enviados à GPU para exibi-lo em uma tela. Se houver um evento de rolagem, a linha de execução do compositor criará outro frame para ser enviado à GPU.

composição
Figura 18: linha de execução do criador criando um frame. O frame é enviado para o processo do navegador e depois para a GPU

A vantagem da composição é que ela é feita sem envolver a linha de execução principal. A linha de execução do criador não precisa esperar o cálculo do estilo ou a execução do JavaScript. É por isso que a composição apenas de animações é considerada a melhor para um desempenho fluido. Se o layout ou a pintura precisar ser calculado novamente, a linha de execução principal precisará estar envolvida.

Resumo

Nesta postagem, analisamos o pipeline de renderização desde a análise até a composição. Esperamos que agora você possa ler mais sobre a otimização de desempenho de um site.

Na próxima e na última postagem desta série, veremos a linha de execução do compositor em mais detalhes e veremos o que acontece quando uma entrada do usuário, como mouse move e click, é recebida.

Você gostou da postagem? Se você tiver dúvidas ou sugestões para a próxima postagem, fique à vontade na seção de comentários abaixo ou com @kosamari no Twitter.

Próximo: a entrada chegará ao compositor