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

Mariko Kosaka

Funcionamento interno de um processo de renderizador

Esta é a terceira parte de uma série de quatro blogs sobre como os navegadores funcionam. Anteriormente, abordamos arquitetura de vários processos e fluxo de navegação. Nesta postagem, vamos analisar o que acontece dentro do processo de renderização.

O processo do renderizador afeta muitos aspectos do desempenho da Web. Como há muitas coisas acontecendo dentro do processo do renderizador, esta postagem é apenas uma visão geral. Se você quiser se aprofundar, a seção Performance dos Fundamentos da Web tem muitos outros recursos.

Os processos do renderizador processam o conteúdo da Web

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

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

Processo do renderizador
Figura 1: processo de 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 raster

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 transformar em um Documento Object Model (DOM).

O DOM é uma representação interna da página de um navegador, bem como a estrutura de dados e a API com que o desenvolvedor da Web pode interagir usando JavaScript.

A análise de um documento HTML em um DOM é definida pelo padrão HTML. Você pode ter notado que o envio de HTML para um navegador nunca gera um erro. Por exemplo, a tag </p> de fechamento ausente é 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 escrito Hi! <b>I'm <i>Chrome</i></b><i>!</i>. Isso ocorre porque a especificação HTML foi projetada para processar esses erros com facilidade. Se você tem curiosidade sobre como essas coisas são feitas, leia a seção "Uma introdução ao processamento de erros e casos estranhos no analisador" da especificação do HTML.

Carregamento de subrecurso

Um site geralmente usa recursos externos, como imagens, CSS e JavaScript. Esses arquivos precisam ser carregados da rede ou do cache. A linha de execução principal pode solicitá-los um por um à medida que os encontra durante a análise para criar um DOM. No entanto, para acelerar, o "scanner de pré-carregamento" é executado simultaneamente. Se houver elementos como <img> ou <link> no documento HTML, o scanner de pré-carregamento vai conferir os tokens gerados pelo analisador de HTML e enviar solicitações para a linha de execução de rede no processo do navegador.

DOM
Figura 2: a linha de execução principal analisando HTML e criando uma árvore DOM

O 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 quê? Porque o JavaScript pode mudar a forma do documento usando elementos como document.write(), que muda toda a estrutura do DOM. A visão geral do modelo de análise na especificação do HTML tem um diagrama bacana. É por isso que o analisador HTML precisa esperar a execução do JavaScript antes de retomar a análise do documento HTML. Se você tem curiosidade sobre o que acontece na execução do JavaScript, a equipe do V8 tem palestras e postagens no blog sobre o assunto.

Indicar ao navegador como você quer carregar recursos

Há muitas maneiras de os desenvolvedores da Web enviarem dicas ao navegador para carregar recursos corretamente. Se o JavaScript não usar 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. Também é possível usar o módulo JavaScript, se for adequado. <link rel="preload"> é uma forma de informar ao navegador que o recurso é definitivamente necessário para a navegação atual e que você quer fazer o download o mais rápido possível. Saiba mais em Priorização de recursos: como usar o navegador para ajudar você.

Cálculo do estilo

Ter um DOM não é suficiente para saber como a página vai ficar, porque podemos estilizar os elementos da página no CSS. A linha de execução principal analisa o CSS e determina o estilo computado para cada nó do DOM. Essas são informações sobre o tipo de estilo aplicado a cada elemento com base em seletores de CSS. Você pode conferir essas informações na seção computed do DevTools.

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

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

Layout

Agora o processo de renderização conhece 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 seu amigo por telefone. "Há um círculo vermelho grande e um quadrado azul pequeno" não é informação suficiente para que seu amigo saiba exatamente como seria a pintura.

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

O layout é um processo para encontrar a geometria dos elementos. A linha de execução principal percorre o DOM e os estilos computados e cria a árvore de layout, que tem informações como coordenadas x y e tamanhos de caixa de limite. A árvore de layout pode ter uma estrutura semelhante à árvore DOM, mas contém apenas informações relacionadas ao que está 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 passando pela árvore DOM com estilos calculados e produzindo a árvore de layout
Figura 6: layout de caixa para um parágrafo que se move devido à mudança de quebra de linha

Determinar o layout de uma página é uma tarefa desafiadora. Até mesmo o layout de página mais simples, como um fluxo de bloco de cima para baixo, precisa considerar o tamanho da fonte e onde fazer quebras de linha, porque elas afetam o tamanho e a forma de um parágrafo, o que afeta onde o parágrafo seguinte precisa estar.

O CSS pode fazer com que o elemento flutue para um lado, mascarar o item de overflow e mudar as direções de escrita. Como você pode imaginar, essa etapa de layout tem uma tarefa poderosa. No Chrome, uma equipe inteira de engenheiros trabalha no layout. Se você quiser saber mais sobre o trabalho deles, confira algumas palestras da BlinkOn Conference, que foram gravadas e são muito interessantes.

Tinta

jogo de desenho
Figura 7: uma pessoa em frente a uma tela segurando um pincel, se perguntando se deve desenhar um círculo ou um quadrado primeiro

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

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

Falha no 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 índice z não foi levado em conta

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 observação do processo de pintura, como "primeiro o plano de fundo, depois o texto e depois o retângulo". Se você já desenhou no elemento <canvas> usando JavaScript, esse processo pode ser familiar para você.

registros de pintura
Figura 9: a linha de execução principal percorrendo a árvore de layout e produzindo registros de pintura

Atualizar o pipeline de renderização é caro

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

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

Se você estiver animando elementos, o navegador terá que executar essas operações entre cada frame. A maioria das telas atualiza 60 vezes por segundo (60 qps). A animação vai parecer suave para os olhos humanos quando você mover coisas pela tela em cada frame. No entanto, se a animação perder os frames no meio, a página vai parecer "desajeitada".

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

Mesmo que suas operações de renderização estejam acompanhando a atualização da tela, esses cálculos estão sendo executados na linha de execução principal, o que significa que ela pode ser bloqueada quando o aplicativo estiver executando JavaScript.

JavaScript causa problemas de jerkiness
Figura 12: frames de animação em uma linha do tempo, mas um frame é bloqueado pelo JavaScript

É possível dividir a operação do JavaScript em pequenos pedaços e programar para executar em cada frame usando requestAnimationFrame(). Para mais informações sobre esse assunto, consulte Otimizar a execução do JavaScript. Você também pode executar o JavaScript em workers da Web para evitar o bloqueio da linha de execução principal.

frame de solicitação de animação
Figura 13: pedaços 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 rasterização simples

Agora que o navegador conhece 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 conversão dessas informações em pixels na tela é chamada de rasterização.

Talvez uma maneira ingênua de lidar com isso seja rasterizar partes dentro da área de visualização. Se um usuário rolar a página, mova o frame rasterizado e preencha as partes ausentes com mais rasterização. É assim que o Chrome lidava com a rasterização quando foi lançado. No entanto, o navegador moderno executa um processo mais sofisticado chamado 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, rasterizar separadamente e compor como uma página em uma linha de execução separada chamada de linha de execução do compositor. Se o rolagem acontecer, já que as camadas já estão rasterizadas, tudo o que ela precisa fazer é compor um novo frame. A animação pode ser feita da mesma forma, movendo camadas e combiná-las em um novo frame.

Você pode conferir como o site é dividido em camadas no DevTools usando o painel de camadas.

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 criar a árvore de camadas. Essa parte é chamada de "Atualizar árvore de camadas" no painel de desempenho do DevTools. Se determinadas partes de uma página que precisam ser uma camada separada (como o menu lateral deslizante) não estiverem recebendo uma, você poderá sugerir ao navegador usando o atributo will-change no CSS.

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

Você pode ter vontade de adicionar camadas a todos os elementos, mas a composição em um número excessivo de camadas pode resultar em uma operação mais lenta do que rasterizar pequenas partes de uma página a cada frame. Portanto, é essencial medir o desempenho de renderização do seu aplicativo. Para saber mais sobre o assunto, consulte Usar apenas propriedades do compositor e gerenciar a contagem de camadas.

Raster 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. A linha de execução do compositor rasteriza cada camada. Uma camada pode ser grande como o comprimento de uma página inteira. Por isso, a linha de execução do compositor as divide em blocos e envia cada bloco para linhas de execução de raster. Os threads rasterizam cada bloco e os armazenam na memória da GPU.

raster
Figura 17: linhas de execução de raster que criam o bitmap de blocos e enviam para a GPU

A linha de execução do compositor pode priorizar diferentes linhas de execução de raster para que as coisas dentro da viewport ou nas proximidades possam ser rasterizadas primeiro. Uma camada também tem vários blocos para diferentes resoluções para processar ações como o zoom.

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

Desenhar quadriláteros Contém informações como o local do bloco na memória e onde na página renderizar o bloco, considerando a composição da página.
Frame do compositor Uma coleção de quads de desenho que representa um frame de uma página.

Um frame do compositor é enviado ao processo do navegador por IPC. Nesse ponto, outro frame do compositor pode ser adicionado da linha de execução da 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 serem mostrados em uma tela. Se um evento de rolagem for recebido, a linha de execução do compositor vai criar outro frame do compositor para ser enviado à GPU.

composto
Figura 18: Linha de execução do compositor criando o frame de composição. O frame é enviado para o processo do navegador e depois para a GPU

O benefício da composição é que ela é feita sem envolver a linha de execução principal. A linha de execução do compositor não precisa esperar pelo cálculo de estilo ou pela execução do JavaScript. É por isso que compor apenas animações é considerado o melhor para um desempenho suave. Se o layout ou a pintura precisar ser calculado novamente, a linha de execução principal precisa estar envolvida.

Conclusão

Nesta postagem, analisamos o pipeline de renderização, da análise à composição. Agora você já pode ler mais sobre a otimização de desempenho de um site.

Na próxima e última postagem desta série, vamos analisar a linha de execução do compositor com mais detalhes e ver o que acontece quando a 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, entre em contato comigo na seção de comentários abaixo ou pelo @kosamari no Twitter.

Em seguida: a entrada está chegando ao compositor