Como o JavaScript funciona: a engine de renderização e dicas para otimizar seu desempenho

Robisson Oliveira
React Brasil
12 min readApr 16, 2019

--

Este é o post #11 da série dedicada a explorar JavaScript e seus componentes de construção. No processo de identificação e descrição dos elementos centrais, também compartilhamos algumas regras práticas que usamos ao criar o SessionStack , um aplicativo JavaScript que precisa ser robusto e altamente eficiente para ajudar os usuários a ver e reproduzir seus defeitos do aplicativo da Web em tempo real.

Abaixo estão os posts dessa série publicados até o momento

Até agora, em nossos posts anteriores da série “Como funciona o JavaScript”, nos concentramos no JavaScript como um idioma, seus recursos, como ele é executado no navegador, como otimizá-lo etc.

Quando você está construindo aplicativos da web, no entanto, você não apenas escreve um código JavaScript isolado que é executado por conta própria. O JavaScript que você escreve está interagindo com o ambiente. Entender esse ambiente, como ele funciona e do que ele é composto permitirá que você crie aplicativos melhores e esteja bem preparado para possíveis problemas que possam surgir quando os aplicativos forem liberados.

Então, vamos ver quais são os principais componentes do navegador:

  • Interface do usuário : isso inclui a barra de endereço, os botões de voltar e avançar, o menu de bookmarking, etc. Em essência, isso é toda parte da exibição do navegador, exceto a janela onde você vê a própria página da web.
  • Mecanismo de navegação : isto lida com as interações entre a interface do usuário e o mecanismo de renderização
  • Engine de renderização : é responsável por exibir a página da web. A engine de renderização analisa o HTML e o CSS e exibe o conteúdo analisado na tela.
  • Rede : são chamadas de rede, como solicitações XHR, feitas usando diferentes implementações para as diferentes plataformas, que estão por trás de uma interface independente de plataforma. Nós falamos sobre a camada de rede em mais detalhes em um post anterior desta série.
  • Backend da interface do usuário : é usado para desenhar os widgets principais, como caixas de seleção e janelas. Este backend expõe uma interface genérica que não é específica da plataforma. Ele usa os métodos de interface do usuário do sistema operacional abaixo.
  • Mecanismo JavaScript : abordamos isso detalhadamente em um post anterior da série. Basicamente, é aqui que o JavaScript é executado.
  • Persistência de dados : seu aplicativo pode precisar armazenar todos os dados localmente. Os tipos suportados de mecanismos de armazenamento incluem localStorage , indexDB , WebSQL e FileSystem .

Neste post, vamos nos concentrar na engine de renderização, já que ela lida com a análise e a visualização do HTML e do CSS, algo com que a maioria dos aplicativos JavaScript interagem constantemente.

Visão geral da engine de renderização

A principal responsabilidade da engine de renderização é exibir a página solicitada na tela do navegador.

As engines de renderização podem exibir documentos e imagens em HTML e XML. Se você estiver usando plug-ins adicionais, as engines também poderão exibir diferentes tipos de documentos, como PDF.

Engines de renderização

Semelhante as engines JavaScript, diferentes navegadores também usam engines de renderização diferentes. Estes são alguns dos mais populares:

  • Geco — Firefox
  • WebKit — Safari
  • Blink — Chrome, Opera (da versão 15 em diante)

O processo de renderização

A engine de renderização recebe o conteúdo do documento solicitado da camada de rede.

Construindo a árvore DOM

A primeira etapa da engine de renderização é analisar o documento HTML e converter os elementos analisados ​​em nós DOM reais em uma árvore DOM .

Imagine que você tenha a seguinte entrada textual:

A árvore DOM deste HTML será parecida com esta:

Basicamente, cada elemento é representado como o nó pai para todos os elementos, que estão diretamente contidos dentro dele. E isso é aplicado recursivamente.

Construindo a árvore CSSOM

CSSOM refere-se ao modelo de objeto CSS . Enquanto o navegador estava construindo o DOM da página, encontrou uma tag de link na seção headque estava referenciando a folha de estilos CSS theme.css externa. Antecipando que possa precisar desse recurso para renderizar a página, ele imediatamente enviou uma solicitação para ele. Vamos imaginar que o arquivo theme.css tenha o seguinte conteúdo:

Assim como no HTML, o mecanismo precisa converter o CSS em algo com o qual o navegador possa trabalhar — o CSSOM. Aqui está como a árvore CSSOM se parecerá:

Você se pergunta por que o CSSOM tem uma estrutura em árvore? Ao calcular o conjunto final de estilos para qualquer objeto na página, o navegador inicia com a regra mais geral aplicável a esse nó (por exemplo, se for filho de um elemento body, então todos os estilos de corpo se aplicam) e recursivamente refina os estilos computados aplicando regras mais específicas.

Vamos trabalhar com o exemplo específico que demos. Qualquer texto contido em uma tag span que é colocada dentro do elemento body, tem um tamanho de fonte de 16 pixels e tem uma cor vermelha. Esses estilos são herdados do elemento body. Se um elemento span for filho de um elemento p , seu conteúdo não será exibido devido aos estilos mais específicos que estão sendo aplicados a ele.

Além disso, observe que a árvore acima não é a árvore completa do CSSOM e apenas mostra os estilos que decidimos substituir em nossa folha de estilo. Cada navegador fornece um conjunto padrão de estilos, também conhecidos como “estilos de agente de usuário” — é o que vemos quando não fornecemos explicitamente nenhum. Nossos estilos simplesmente substituem esses padrões.

Construindo a árvore de renderização

As instruções visuais no HTML, combinadas com os dados de estilo da árvore CSSOM, estão sendo usadas para criar uma árvore de renderização .

O que é uma árvore de renderização que você pode perguntar? Esta é uma árvore dos elementos visuais construída na ordem em que eles serão exibidos na tela. É a representação visual do HTML junto com o CSS correspondente. O objetivo desta árvore é permitir a pintura do conteúdo em sua ordem correta.

Cada nó na árvore de renderização é conhecido como renderizador ou objeto de renderização no Webkit.

É assim que a árvore de renderizadores das árvores DOM e CSSOM acima se parecerá:

Para construir a árvore de renderização, o navegador faz aproximadamente o seguinte:

  • Começando na raiz da árvore DOM, ela percorre cada nó visível. Alguns nós não são visíveis (por exemplo, tags de script, metatags e assim por diante) e são omitidos, pois não são refletidos na saída renderizada. Alguns nós são ocultados via CSS e também são omitidos da árvore de renderização. Por exemplo, o nó span — no exemplo acima, ele não está presente na árvore de renderização porque temos uma regra explícita que define a display: none propriedade nela.
  • Para cada nó visível, o navegador encontra as regras CSSOM correspondentes e as aplica.
  • Emite nós visíveis com conteúdo e seus estilos computados

Você pode dar uma olhada no código-fonte do RenderObject (no WebKit) aqui: https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h

Vamos apenas olhar algumas das principais coisas para esta classe:

Cada renderizador representa uma área retangular que geralmente corresponde à caixa CSS de um nó. Inclui informações geométricas como largura, altura e posição.

Layout da árvore de renderização

Quando o renderizador é criado e adicionado à árvore, ele não possui posição e tamanho. O cálculo desses valores é chamado de layout.

O HTML usa um modelo de layout baseado em fluxo, o que significa que na maioria das vezes ele pode calcular a geometria em uma única passagem. O sistema de coordenadas é relativo ao renderizador de raiz. As coordenadas superior e esquerda são usadas.

Layout é um processo recursivo — começa no renderizador de raiz, que corresponde ao elemento <html> do documento HTML. O layout continua recursivamente por meio de uma parte ou de toda a hierarquia do renderizador, computando as informações geométricas de cada representante que o exige.

A posição do renderizador de raiz é 0,0 e suas dimensões têm o tamanho da parte visível da janela do navegador (também conhecida como viewport).

Iniciar o processo de layout significa dar a cada nó as coordenadas exatas onde ele deve aparecer na tela.

Pintando a árvore de renderização

Nesse estágio, a árvore do renderizador é percorrida e o método paint() do renderizador é chamado para exibir o conteúdo na tela.

A pintura pode ser global ou incremental (semelhante ao layout):

  • Global — toda a árvore é repintada.
  • Incremental — apenas alguns dos representantes mudam de uma maneira que não afeta a árvore inteira. O renderizador invalida seu retângulo na tela. Isso faz com que o sistema operacional o veja como uma região que precisa de repintura e para gerar um evento de paint . O SO faz isso de maneira inteligente, mesclando várias regiões em uma.

Em geral, é importante entender que a pintura é um processo gradual. Para melhor UX, o mecanismo de renderização tentará exibir o conteúdo na tela o mais rápido possível. Ele não esperará até que todo o HTML seja analisado para começar a criar e exibir a árvore de renderização. Partes do conteúdo serão analisadas e exibidas, enquanto o processo continua com o restante dos itens de conteúdo que continuam vindo da rede.

Ordem de processamento de scripts e folhas de estilo

Os scripts são analisados ​​e executados imediatamente quando o analisador alcança uma tag <script>. A análise do documento é interrompida até que o script seja executado. Isso significa que o processo é síncrono .

Se o script é externo, primeiro ele precisa ser obtido da rede (também de forma síncrona). Toda a análise é interrompida até que a busca seja concluída.

O HTML5 adiciona uma opção para marcar o script como assíncrono para que seja analisado e executado por um thread diferente.

Otimizando o desempenho de renderização

Se você quiser otimizar seu aplicativo, há cinco áreas principais nas quais você precisa se concentrar. Estas são as áreas sobre as quais você tem controle:

  1. JavaScript — em posts anteriores nós cobrimos o tópico de escrever código otimizado que não bloqueia a interface do usuário, é eficiente na memória, etc. Quando se trata de renderização, precisamos pensar sobre a maneira como seu código JavaScript irá interagir com os elementos DOM a página.O JavaScript pode criar muitas mudanças na interface do usuário, especialmente em SPAs.
  2. Cálculos de estilo — este é o processo de determinar qual regra CSS se aplica a qual elemento com base nos seletores correspondentes. Depois que as regras são definidas, elas são aplicadas e os estilos finais de cada elemento são calculados.
  3. Layout — uma vez que o navegador saiba quais regras se aplicam a um elemento, ele pode começar a calcular quanto espaço ocupa este último e onde ele está localizado na tela do navegador. O modelo de layout da web define que um elemento pode afetar outros. Por exemplo, a largura do <body> pode afetar a largura de seus filhos e assim por diante. Isso tudo significa que o processo de layout é computacionalmente intensivo. O desenho é feito em várias camadas.
  4. Paint — é onde os pixels reais estão sendo preenchidos. O processo inclui o desenho de texto, cores, imagens, bordas, sombras, etc. — todas as partes visuais de cada elemento.
  5. Composição — como as partes da página foram desenhadas em várias camadas, elas precisam ser desenhadas na tela na ordem correta, para que a página seja processada corretamente. Isso é muito importante, especialmente para elementos sobrepostos.

Otimizando seu JavaScript

O JavaScript geralmente aciona alterações visuais no navegador. Tanto mais quando se constrói um SPA.

Aqui estão algumas dicas sobre quais partes do seu JavaScript você pode otimizar para melhorar a renderização:

  • Evite setTimeout ou setInterval para atualizações visuais. Estes invocarão o callback em algum momento no frame, possível no final. O que queremos fazer é acionar a mudança visual logo no início do quadro para não errar.
  • Mova os cálculos de JavaScript de longa duração para os Web Workers como discutimos anteriormente.
  • Use micro-tarefas para introduzir as alterações do DOM em vários quadros. Isso ocorre no caso de as tarefas precisarem de acesso ao DOM, que não é acessível por Web Workers. Isso basicamente significa que você dividiria uma grande tarefa em tarefas menores e as executaria dentro de requestAnimationFrame, setTimeout, setInterval, dependendo da natureza da tarefa.

Otimize seu CSS

Modificar o DOM através da adição e remoção de elementos, alterar atributos, etc., fará com que o navegador recalcule os estilos dos elementos e, em muitos casos, o layout de toda a página ou, pelo menos, de partes dela.

Para otimizar a renderização, considere o seguinte:

  • Reduza a complexidade dos seus seletores. A complexidade do seletor pode levar mais de 50% do tempo necessário para calcular os estilos de um elemento, em comparação com o restante do trabalho que está construindo o próprio estilo.
  • Reduza o número de elementos nos quais o cálculo de estilo deve acontecer. Em essência, faça alterações de estilo em alguns elementos diretamente, em vez de invalidar a página como um todo.

Otimizar o layout

Re-cálculos de layout podem ser muito pesados ​​para o navegador. Considere as seguintes otimizações:

  • Reduza o número de layouts sempre que possível. Quando você altera estilos, o navegador verifica se alguma das alterações exige que o layout seja recalculado. Alterações em propriedades como largura, altura, esquerda, superior e, em geral, propriedades relacionadas à geometria exigem layout. Portanto, evite alterá-los o máximo possível.
  • Use o flexbox em modelos de layout mais antigos sempre que possível. Ele funciona mais rápido e pode criar uma enorme vantagem de desempenho para seu aplicativo.
  • Evite layouts síncronos forçados. O importante é ter em mente que enquanto o JavaScript é executado, todos os valores de layout antigos do quadro anterior são conhecidos e estão disponíveis para você consultar. Se você acessar box.offsetHeight isso não será um problema. Se você, no entanto, alterar os estilos da caixa antes que ela seja acessada (por exemplo, adicionando dinamicamente alguma classe CSS ao elemento), o navegador terá primeiro que aplicar a mudança de estilo e depois executar o layout. Isso pode consumir muito tempo e consumir muitos recursos, portanto, evite-o sempre que possível.

Otimize o paint

Isso geralmente é a execução mais longa de todas as tarefas, por isso é importante evitá-lo tanto quanto possível. Aqui está o que podemos fazer:

  • Alterar qualquer propriedade que não seja transformações ou opacidade aciona uma pintura. Use com moderação.
  • Se você acionar um layout, também acionará uma pintura, pois a alteração da geometria resulta em uma alteração visual do elemento.
  • Reduza as áreas de pintura através da promoção de camadas e orquestração de animações.

A renderização é um aspecto vital de como o SessionStack funciona. O SessionStack tem que recriar como um vídeo tudo o que aconteceu com seus usuários no momento em que eles tiveram um problema ao navegar pelo seu aplicativo da web. Para fazer isso, o SessionStack aproveita apenas os dados que foram coletados por nossa biblioteca: eventos do usuário, mudanças no DOM, solicitações de rede, exceções, mensagens de depuração, etc. Nosso player é altamente otimizado para renderizar e fazer uso de todos os dados coletados para oferecer uma simulação de pixel perfeito do navegador de seus usuários e tudo que aconteceu nele, tanto visualmente quanto tecnicamente.

Existe um plano gratuito se você quiser experimentar o SessionStack .

Referências

Este é um artigo traduzido com a autorização do autor. O artigo original pode ser lido em https://blog.sessionstack.com/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance-7b95553baeda

Autor do post original — Alexander Zlatkov — Co-founder & CEO @SessionStack

--

--

Robisson Oliveira
React Brasil

Senior Cloud Application Architect at Amazon Web Services(AWS). My personal reflections on software development