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 do blog sobre como os navegadores funcionam. Anteriormente, abordamos a arquitetura de vários processos e o fluxo de navegação. Nesta postagem, veremos o que acontece dentro do processo do renderizador.

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

Os processos do renderizador processam o conteúdo da Web

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

A função principal 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 transformar em um Document object Model (DOM).

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

A análise de um documento HTML em um DOM é definida pelo padrão HTML. Você deve ter notado que fornecer HTML a um navegador nunca gera um erro. Por exemplo, a falta 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 escrito Hi! <b>I'm <i>Chrome</i></b><i>!</i>. Isso ocorre porque a especificação HTML foi projetada para lidar com esses erros corretamente. Se você tiver curiosidade de como isso é feito, leia a seção "Uma introdução ao tratamento de erros e casos estranhos no analisador" da especificação HTML.

Carregamento de recursos secundários

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 poderia solicitá-las uma a uma, já que elas são encontradas durante a análise para criar um DOM, mas, para acelerar, o "verificador de pré-carregamento" é executado de maneira simultânea. 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 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ê? Como o JavaScript pode mudar a forma do documento usando recursos 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 esperar a execução do JavaScript antes de retomar a análise do documento HTML. Se você tiver curiosidade sobre o que acontece na execução do JavaScript, a equipe do V8 tem palestras e postagens do blog sobre isso (link em inglês).

Dica para navegar como você quer carregar recursos

Há muitas maneiras de os desenvolvedores da Web enviarem dicas ao navegador para carregar os recursos adequadamente. 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. Você também pode usar o módulo JavaScript, se isso for adequado. <link rel="preload"> é uma maneira 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 saber como seria a aparência da página, porque podemos definir o estilo dos elementos dela no CSS. A linha de execução principal analisa o CSS e determina o estilo calculado para cada nó DOM. Essas informações são sobre o tipo de estilo aplicado a cada elemento com base nos seletores de CSS. Consulte 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 calculado. A tag <h1> é exibida maior do 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, consulte o código-fonte aqui.

Layout

Agora, o processo do renderizador conhece a estrutura de um documento e os estilos para 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 caixa delimitadora. A árvore de layout pode ser semelhante à árvore do 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 uma pseudoclasse com conteúdo como p::before{content:"Hi!"} for aplicada, ela será incluída na árvore de layout, mesmo que isso não esteja no DOM.

layout.
Figura 5: a linha de execução principal repassando a árvore do DOM com estilos calculados e árvore de layout de produção
Figura 6: layout de caixa de um parágrafo que se move devido a uma 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 quebrá-la, porque eles afetam o tamanho e a forma de um parágrafo, o que afeta onde o parágrafo seguinte precisa estar.

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

Tinta

jogo de desenho
Figura 7: uma pessoa em frente a uma tela segurando um pincel pensando se deveria desenhar um círculo ou um quadrado primeiro.

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 o local dos elementos, mas ainda precisa julgar a ordem em que eles serão pintados.

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

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

Nessa 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 “segundo plano primeiro, depois texto e depois retângulo”. Se você 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 de DOM+Style, Layout e Paint na ordem em que são geradas

O 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 gerada novamente para as partes afetadas do documento.

Se você estiver animando elementos, o navegador precisará executar estas operações entre cada quadro. A maioria das telas atualiza a tela 60 vezes por segundo (60 fps). A animação é exibida suave para os olhos humanos quando você move elementos na tela em cada frame. No entanto, se a animação não tiver os frames intermediários, a página vai parecer "instável".

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 serão executados na linha de execução principal, o que significa que ela poderá ser bloqueada quando o aplicativo estiver executando JavaScript.

instabilidade do jage por JavaScript
Figura 12: frames de animação em uma linha do tempo, mas um frame está bloqueado pelo JavaScript.

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

solicitar frame de animaçã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 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? Transformar essas informações em pixels na tela é chamado de varredura.

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

O que é a 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 linha de execução de composição. Se a rolagem acontecer, já que as camadas já foram rasterizadas, basta criar um novo frame. A animação pode ser feita da mesma maneira ao mover camadas e compor um novo frame.

Para conferir como o site é dividido em camadas no DevTools, use 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 criar a árvore de camadas. Essa parte é chamada de "Update Layer Tree" 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 passando pela árvore de layout que produz a árvore de camadas

Você pode ficar tentado a dar camadas a cada elemento, mas a composição em um número excessivo de camadas pode resultar em uma operação mais lenta do que a varredura de pequenas partes de uma página a cada frame. Portanto, é fundamental que você meça o desempenho da renderização do aplicativo. Para saber mais sobre esse assunto, consulte Usar apenas propriedades do compositor 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 para a linha de execução do compositor. Em seguida, o encadeamento do compositor faz a varredura de cada camada. Uma camada pode ser grande como a extensão de uma página. Por isso, a linha de execução do compositor divide em blocos e envia cada bloco para linhas de execução de varredura. As linhas de execução de varredura fazem a varredura de cada bloco e os armazenam na memória da GPU.

varredura
Figura 17: linhas de execução de varredura criando o bitmap de blocos e enviando para a GPU

A linha de execução do compositor pode priorizar diferentes linhas de execução de varredura para que os itens dentro da janela de visualização (ou próximos) possam ser rasterizados primeiro. Uma camada também tem vários blocos para resoluções diferentes para processar 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 bloco chamadas draw quads para criar um frame do compositor.

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

Um frame de compositor é então enviado ao processo do navegador via IPC. Nesse ponto, outro frame de compositor pode ser adicionado a partir da linha de execução de IU para a mudança da IU do navegador ou de outros processos do renderizador para extensões. Esses frames do compositor são enviados à GPU para que eles sejam exibidos em uma tela. Se um evento de rolagem chegar, a linha de execução do compositor criará outro frame para ser enviado à GPU.

composto
Figura 18: linha de execução de composição 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 compostor 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 uma performance suave. Se o layout ou a pintura precisar ser calculado novamente, a linha de execução principal precisará estar envolvida.

conclusão

Nesta postagem, falamos sobre a renderização do pipeline da análise para a composição. Esperamos que agora você consiga ler mais sobre a otimização da performance de um site.

Na próxima e na última postagem desta série, analisaremos 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, entra.

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

Próximo: a entrada está chegando para o compositor