Como os navegadores funcionam

Nos bastidores dos navegadores da Web modernos

Prefácio

Este guia abrangente sobre as operações internas do WebKit e do Gecko é o resultado de muita pesquisa feita pela desenvolvedora israelense Tali Garsiel. Ao longo de alguns anos, ela revisou todos os dados publicados sobre os aspectos internos do navegador e passou muito tempo lendo o código-fonte do navegador da Web. Ela escreveu:

Como desenvolvedor da Web, aprender o funcionamento interno das operações de navegador ajuda você a tomar decisões melhores e a saber as justificativas por trás das práticas recomendadas de desenvolvimento. Embora este documento seja longo, recomendamos que você reserve um tempo para se aprofundar. Você não vai se arrepender.

Paul Irlanda, Relações com desenvolvedores do Chrome

Introdução

Os navegadores da Web são os softwares mais amplamente utilizados. Nesta introdução, explico como eles funcionam nos bastidores. Veremos o que acontece quando você digita google.com na barra de endereço até ver a página do Google na tela do navegador.

Navegadores que abordaremos

Atualmente, existem cinco principais navegadores usados em computadores: Chrome, Internet Explorer, Firefox, Safari e Opera. No celular, os principais navegadores são: Android Browser, iPhone, Opera Mini e Opera Mobile, UC Browser, Nokia S40/S60 e Chrome. Todos eles são baseados no WebKit, exceto os navegadores Opera. Vou dar exemplos dos navegadores de código aberto Firefox e Chrome e do Safari (que é parcialmente de código aberto). De acordo com estatísticas da StatCounter (em junho de 2013), o Google Chrome, o Firefox e o Safari representam cerca de 71% do uso global de navegadores para computadores. Em dispositivos móveis, o navegador do Android, o iPhone e o Chrome representam cerca de 54% do uso.

A principal funcionalidade do navegador

A principal função do navegador é apresentar o recurso da Web que você escolher, solicitando-o ao servidor e exibindo-o na janela do navegador. O recurso geralmente é um documento HTML, mas também pode ser um PDF, uma imagem ou algum outro tipo de conteúdo. O local do recurso é especificado pelo usuário com um URI (Identificador de recurso uniforme).

A forma como o navegador interpreta e exibe arquivos HTML é feita nas especificações de HTML e CSS. Essas especificações são mantidas pela organização W3C (World Wide Web Consortium), que é a organização de padrões para a Web. Por anos, os navegadores mantiveram-se apenas com uma parte das especificações e desenvolveram as próprias extensões. Isso causou sérios problemas de compatibilidade para autores da Web. Hoje, a maioria dos navegadores está mais ou menos em conformidade com as especificações.

As interfaces de usuário dos navegadores têm muito em comum entre si. Entre os elementos comuns de interface do usuário estão:

  1. Barra de endereço para inserir um URI
  2. Botões "Voltar" e "Avançar"
  3. Opções para adicionar aos favoritos
  4. Botões de atualização e parada para atualizar ou interromper o carregamento de documentos atuais
  5. Botão "Início" que leva você para a página inicial

Estranhamente, a interface de usuário do navegador não está em nenhuma especificação formal, apenas vem de boas práticas moldadas ao longo de anos de experiência e de navegadores que imitam uns aos outros. A especificação HTML5 não define elementos de IU que um navegador precisa ter, mas lista alguns elementos comuns. Entre elas, estão a barra de endereço, a barra de status e a barra de ferramentas. É claro que existem recursos exclusivos para um navegador específico, como o gerenciador de downloads do Firefox.

Infraestrutura de alto nível

Os principais componentes do navegador são:

  1. Interface do usuário: inclui a barra de endereço, o botão "Voltar/avançar", o menu de favoritos etc. Todas as partes da tela do navegador, exceto a janela em que você vê a página solicitada.
  2. O mecanismo de navegador: organiza ações entre a interface e o mecanismo de renderização.
  3. O mecanismo de renderização: responsável pela exibição do conteúdo solicitado. Por exemplo, se o conteúdo solicitado for HTML, o mecanismo de renderização analisará HTML e CSS e exibirá o conteúdo analisado na tela.
  4. Rede: para chamadas de rede, como solicitações HTTP, usando diferentes implementações para diferentes plataformas por trás de uma interface independente da plataforma.
  5. Back-end da interface: usado para desenhar widgets básicos, como caixas de combinação e janelas. Esse back-end expõe uma interface genérica que não é específica à plataforma. Abaixo dela, são usados métodos de interface do usuário do sistema operacional.
  6. Intérprete JavaScript. Usado para analisar e executar o código JavaScript.
  7. Armazenamento de dados. Essa é uma camada de persistência. O navegador pode precisar salvar todos os tipos de dados localmente, como cookies. Os navegadores também oferecem suporte a mecanismos de armazenamento como localStorage, IndexedDB, WebSQL e FileSystem.
Componentes do navegador
Figura 1: componentes do navegador

É importante observar que navegadores como o Chrome executam várias instâncias do mecanismo de renderização: uma para cada guia. Cada guia é executada em um processo separado.

Mecanismos de renderização

A responsabilidade do mecanismo de renderização é a renderização, ou seja, a exibição do conteúdo solicitado na tela do navegador.

Por padrão, o mecanismo de renderização pode exibir documentos e imagens HTML e XML. Ele pode exibir outros tipos de dados por meio de plug-ins ou extensões; por exemplo, exibindo documentos PDF usando um plug-in de visualizador de PDF. No entanto, neste capítulo vamos nos concentrar no caso de uso principal: exibir HTML e imagens formatadas com CSS.

Navegadores diferentes usam mecanismos de renderização distintos: o Internet Explorer usa o Trident, o Firefox usa o Gecko, o Safari usa o WebKit. O Chrome e o Opera (a partir da versão 15) usam o Blink, um garfo do WebKit.

O WebKit é um mecanismo de renderização de código aberto que começou como um mecanismo para a plataforma Linux e foi modificado pela Apple para ser compatível com Mac e Windows.

Fluxo principal

O mecanismo de renderização começará a receber o conteúdo do documento solicitado da camada de rede. Isso geralmente será feito em blocos de 8 KB.

Depois disso, este é o fluxo básico do mecanismo de renderização:

Fluxo básico do mecanismo de renderização
Figura 2: fluxo básico do mecanismo de renderização

O mecanismo de renderização inicia a análise do documento HTML e converte elementos em nós DOM em uma árvore chamada "árvore de conteúdo". O mecanismo analisará os dados de estilo, tanto em arquivos CSS externos quanto nos elementos de estilo. As informações de estilo e as instruções visuais no HTML serão usadas para criar outra árvore: a árvore de renderização.

A árvore de renderização contém retângulos com atributos visuais como cor e dimensões. Os retângulos estão na ordem correta para serem exibidos na tela.

Após a construção da árvore de renderização, ela passa por um processo de layout. Isso significa dar a cada nó as coordenadas exatas em que ele deve aparecer na tela. A próxima etapa é a pintura: a árvore de renderização será atravessada e cada nó será pintado usando a camada de back-end da interface.

É importante entender que esse é um processo gradual. Para uma melhor experiência do usuário, o mecanismo de renderização tenta exibir o conteúdo na tela o mais rápido possível. Ele não vai esperar até que todo o HTML seja analisado para começar a criar e definir o layout da árvore de renderização. Partes do conteúdo são analisadas e exibidas, enquanto o processo continua com o restante do conteúdo que vem da rede.

Principais exemplos de fluxo

Fluxo principal do WebKit.
Figura 3: fluxo principal do WebKit
Fluxo principal do mecanismo de renderização Gecko do Mozilla.
Figura 4: fluxo principal do mecanismo de renderização Gecko do Mozilla

As figuras 3 e 4 mostram que, embora WebKit e Gecko usem terminologias um pouco diferentes, o fluxo é basicamente o mesmo.

A Gecko chama a árvore de elementos formatados visualmente de "Árvore de frames". Cada elemento é um frame. O WebKit usa o termo "Árvore de renderização" e consiste em "Objetos de renderização". O WebKit usa o termo "layout" para o posicionamento dos elementos, enquanto o Gecko o chama de "Reflow". "Anexo" é o termo do WebKit para conectar nós DOM e informações visuais para criar a árvore de renderização. Uma pequena diferença não semântica é que o Gecko tem uma camada extra entre o HTML e a árvore DOM. Ele é chamado de "coletor de conteúdo" e é uma fábrica para criar elementos DOM. Falaremos sobre cada parte do fluxo:

Análise - geral

Como a análise é um processo importante no mecanismo de renderização, vamos abordá-la um pouco mais. Vamos começar com uma breve introdução sobre a análise.

Analisar um documento significa traduzi-lo para uma estrutura que o código possa usar. O resultado da análise geralmente é uma árvore de nós que representa a estrutura do documento. Isso é chamado de árvore de análise ou árvore de sintaxe.

Por exemplo, a análise da expressão 2 + 3 - 1 pode retornar esta árvore:

Nó da árvore de expressão matemática.
Figura 5: nó da árvore da expressão matemática

Gramática

A análise é baseada nas regras de sintaxe que o documento segue: a linguagem ou formato em que foi escrito. Todo formato que pode ser analisado deve ter gramática determinista composta de regras de vocabulário e sintaxe. Ela é chamada de gramática livre de contexto. As linguagens humanas não são essas linguagens e, por isso, não podem ser analisadas com técnicas de análise convencionais.

Analisador - Combinação de léxicos

A análise pode ser separada em dois subprocessos: análise léxica e análise sintática.

A análise léxica é o processo de dividir a entrada em tokens. Os tokens são o vocabulário da linguagem: a coleção de elementos básicos válidos. Na linguagem humana, isso consistirá em todas as palavras que aparecem no dicionário daquele idioma.

A análise sintática é a aplicação das regras de sintaxe da linguagem.

Os analisadores dividem o trabalho em dois componentes: o analisador léxico (às vezes chamado tokenizador), responsável por dividir a entrada em tokens válidos, e o analisador, responsável por construir a árvore de análise ao analisar a estrutura do documento de acordo com as regras de sintaxe da linguagem.

O analisador léxico sabe como eliminar caracteres irrelevantes como espaços em branco e quebras de linha.

Do documento de origem para a análise de árvores
Figura 6: do documento de origem para as árvores de análise

O processo de análise é iterativo. O analisador solicita ao analisador léxico um novo token e tenta combiná-lo com uma das regras de sintaxe. Se uma regra corresponder, um nó correspondente ao token será adicionado à árvore de análise e o analisador solicitará outro token.

Se nenhuma regra corresponder, o analisador armazenará o token internamente e continuará solicitando tokens até que uma regra correspondente a todos os tokens armazenados internamente seja encontrada. Se nenhuma regra for encontrada, o analisador gerará uma exceção. Isso significa que o documento não era válido e continha erros de sintaxe.

Tradução

Em muitos casos, a árvore de análise não é o produto final. A análise é frequentemente usada em traduções: transformar o documento de entrada em outro formato. Um exemplo é a compilação. O compilador que compila o código-fonte no código de máquina primeiro o analisa em uma árvore de análise e, em seguida, traduz a árvore em um documento de código de máquina.

Fluxo de compilação
Figura 7: fluxo de compilação

Exemplo de análise

Na figura 5, construímos uma árvore sintática a partir de uma expressão matemática. Vamos tentar definir uma linguagem matemática simples e conhecer o processo de análise.

Sintaxe:

  1. Os elementos básicos da sintaxe da linguagem são expressões, termos e operações.
  2. Nossa linguagem pode incluir qualquer número de expressões.
  3. Uma expressão é definida como um "termo" seguido por uma "operação" seguida por outro termo
  4. Uma operação é um token mais ou menos
  5. Um termo é um token inteiro ou uma expressão

Vamos analisar a entrada 2 + 3 - 1.

A primeira substring que corresponde a uma regra é 2: conforme a regra no 5, é um termo. A segunda correspondência é 2 + 3, que corresponde à terceira regra: um termo seguido por uma operação seguida por outro termo. A próxima correspondência só será atingida no final da entrada. 2 + 3 - 1 é uma expressão porque já sabemos que 2 + 3 é um termo, então temos um termo seguido por uma operação seguida por outro termo. 2 + + não corresponde a nenhuma regra e, portanto, é uma entrada inválida.

Definições formais para vocabulário e sintaxe

O vocabulário geralmente é expresso por expressões regulares.

Por exemplo, nossa linguagem será definida como:

INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -

Como mostrado, os números inteiros são definidos por uma expressão regular.

A sintaxe geralmente é definida em um formato chamado BNF. Nossa linguagem é definida da seguinte forma:

expression :=  term  operation  term
operation :=  PLUS | MINUS
term := INTEGER | expression

Dissemos que uma linguagem pode ser analisada por analisadores regulares se a gramática for livre de contexto. Uma definição intuitiva de gramática livre de contexto é uma gramática que pode ser totalmente expressa em BNF. Para uma definição formal, consulte o artigo da Wikipédia sobre gramática livre de contexto.

Tipos de analisadores

Existem dois tipos de analisadores: de cima para baixo e de baixo para cima. Uma explicação intuitiva é que os analisadores de cima para baixo examinam a estrutura de alto nível da sintaxe e tentam encontrar uma correspondência de regra. Os analisadores Bottom Up começam com a entrada e a transformam gradualmente nas regras de sintaxe, começando pelas regras de baixo nível até que as regras de alto nível sejam atendidas.

Vamos conferir como os dois tipos de analisadores analisarão nosso exemplo.

O analisador descendente começa pela regra de nível superior: ele identifica 2 + 3 como uma expressão. Em seguida, ela vai identificar 2 + 3 - 1 como uma expressão. O processo de identificação da expressão evolui, correspondendo às outras regras, mas o ponto inicial é a regra de nível mais alto.

O analisador Bottom Up faz a leitura da entrada até que uma regra seja encontrada. Ela substitui a entrada correspondente pela regra. Isso acontecerá até o final da entrada. A expressão parcialmente correspondente é colocada na pilha do analisador.

Stack Entrada
2 + 3 - 1
term 3 ou 1
operação do termo 3 a 1
expressão - 1
operação de expressão 1
expressão -

Esse tipo de analisador é chamado de analisador shift-reduce, porque a entrada é deslocada para a direita (imagine um ponteiro apontando primeiro para o início da entrada e se movendo para a direita) e é gradualmente reduzida às regras de sintaxe.

Geração automática de analisadores

Algumas ferramentas podem gerar um analisador. Você alimenta os estudantes com a gramática da sua linguagem (suas regras de vocabulário e sintaxe) e eles geram um analisador em funcionamento. A criação de um analisador exige conhecimento profundo sobre a análise, e não é fácil criar um analisador otimizado manualmente, então os geradores de analisador podem ser muito úteis.

O WebKit usa dois geradores de analisador conhecidos: Flex para criar um analisador léxico e Bison para criar um analisador. É possível encontrar esses geradores com os nomes "Lex" e "Ycc". A entrada do Flex é um arquivo que contém definições de expressão regular dos tokens. A entrada do Bison são as regras de sintaxe da linguagem no formato BNF.

Analisador de HTML

A função do analisador HTML é analisar a marcação HTML em uma árvore de análise.

Gramática HTML

O vocabulário e a sintaxe de HTML são definidos em especificações criadas pela organização W3C.

Como vimos na introdução à análise, a sintaxe gramatical pode ser definida formalmente com o uso de formatos como BNF.

Infelizmente, todos os tópicos convencionais sobre analisadores não se aplicam ao HTML (não os mencionei só por diversão; eles serão usados na análise de CSS e JavaScript). O HTML não pode ser facilmente definido por uma gramática livre de contexto necessária para os analisadores.

Há um formato formal para definição de HTML, DTD (Definição de Tipo de Documento), mas não é uma gramática livre de contexto.

Isso pode parecer estranho à primeira vista, mas o HTML é bem semelhante ao XML. Há muitos analisadores de XML disponíveis. Existe uma variação XML de HTML/XHTML; então, qual é a grande diferença?

A diferença é que a abordagem HTML é mais permissiva: ela permite omitir certas tags (que são então adicionadas implicitamente) ou, às vezes, omitir tags de início ou fim e assim por diante. Em geral, é uma sintaxe "leve", ao contrário da sintaxe rígida e exigente do XML.

Esse detalhe aparentemente pequeno faz toda a diferença. Por um lado, esta é a principal razão pela qual o HTML é tão popular: ele perdoa seus erros e facilita a vida do autor da web. Por outro lado, dificulta a escrita de uma gramática formal. Resumindo, o HTML não pode ser facilmente analisado por analisadores convencionais, pois sua gramática não está livre de contexto. O HTML não pode ser analisado por analisadores de XML.

DTD HTML

A definição HTML está em um formato DTD. Esse formato é usado para definir os idiomas da família SGML. O formato contém definições para todos os elementos permitidos, seus atributos e hierarquia. Como vimos anteriormente, o DTD HTML não forma uma gramática livre de contexto.

Existem algumas variações do DTD. O modo estrito está em conformidade somente com as especificações, mas outros modos são compatíveis com marcações usadas por navegadores no passado. O objetivo é oferecer compatibilidade com versões anteriores de conteúdo. O DTD restrito atual está aqui: www.w3.org/TR/html4/strict.dtd (link em inglês)

DOM

A árvore de saída (a "árvore de análise") é uma árvore de elementos DOM e nós de atributos. DOM é a abreviação de Document Object Model (modelo de objeto de documentos). É a apresentação de objeto do documento HTML e a interface de elementos HTML para o mundo externo, como o JavaScript.

A raiz da árvore é o objeto "Document".

O DOM tem uma relação quase direta com a marcação. Exemplo:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

Essa marcação seria convertida para a seguinte árvore do DOM:

Árvore do DOM da marcação de exemplo
Figura 8: árvore DOM da marcação de exemplo

Assim como o HTML, o DOM é especificado pela organização W3C. Acesse www.w3.org/DOM/DOMTR (link em inglês). É uma especificação genérica para manipular documentos. Um módulo específico descreve elementos específicos do HTML. As definições de HTML podem ser encontradas aqui: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html (link em inglês).

Quando digo que a árvore contém nós DOM, quero dizer que ela é construída com elementos que implementam uma das interfaces do DOM. Os navegadores usam implementações concretas com outros atributos usados internamente pelo navegador.

O algoritmo de análise

Como vimos nas seções anteriores, o HTML não pode ser analisado utilizando os analisadores descendentes e superiores comuns.

Estes são os motivos:

  1. A natureza permissiva da linguagem.
  2. Os navegadores têm tolerância a erros tradicionais para oferecer suporte a casos conhecidos de HTML inválido.
  3. O processo de análise é reentrante. Para outras linguagens, a origem não muda durante a análise. No entanto, em HTML, o código dinâmico (como elementos de script que contêm chamadas document.write()) pode adicionar tokens extras. Portanto, o processo de análise modifica a entrada.

Como não podem usar as técnicas de análise normais, os navegadores criam analisadores personalizados para analisar HTML.

O algoritmo de análise é descrito em detalhes na especificação do HTML5. O algoritmo consiste em dois estágios: tokenização e construção da árvore.

A tokenização é a análise léxica, ou seja, a análise da entrada em tokens. Entre os tokens HTML estão as tags de início, tags finais, nomes e valores de atributos.

O tokenizador reconhece o token, fornece-o ao construtor da árvore e consome o próximo caractere para reconhecer o próximo token e assim por diante até o final da entrada.

Fluxo de análise HTML (extraído de especificação de HTML5)
Figura 9: fluxo de análise HTML (retirado das especificações do HTML5)

O algoritmo de tokenização

A saída do algoritmo é um token HTML. O algoritmo é expresso como uma máquina de estados. Cada estado consome um ou mais caracteres do fluxo de entrada e atualiza o estado seguinte de acordo com esses caracteres. A decisão é influenciada pelo estado atual da tokenização e pelo estado de construção da árvore. Isso significa que o mesmo caractere consumido produz resultados diferentes para o próximo estado correto, dependendo do estado atual. O algoritmo é complexo demais para ser descrito por completo, então vamos conferir um exemplo simples que vai nos ajudar a entender o princípio.

Exemplo básico: tokenização do seguinte HTML:

<html>
  <body>
    Hello world
  </body>
</html>

O estado inicial é o "Estado de dados". Quando o caractere < é encontrado, o estado muda para "Estado de tag aberta". O consumo de um caractere a-z causa a criação de um "Token de tag de início". O estado é alterado para "Estado do nome da tag". Esse estado permanece até que o caractere > seja consumido. Cada caractere é anexado ao novo nome do token. No nosso caso, o token criado é um html.

Quando a tag > é alcançada, o token atual é emitido e o estado volta para o "Estado de dados". A tag <body> será tratada pelas mesmas etapas. Até agora, as tags html e body foram emitidas. Agora voltamos ao "Estado de dados". O consumo do caractere H de Hello world causa a criação e emissão de um token de caractere. Isso acontece até que o < de </body> seja alcançado. Emitiremos um token de caractere para cada caractere de Hello world.

Agora voltamos ao estado"Tag aberta". O consumo da próxima entrada / causa a criação de um end tag token e o "Estado do nome da tag". Novamente, permanecemos nesse estado até alcançarmos >.Em seguida, o token da nova tag é emitido e voltamos ao "Estado de dados". A entrada </html> será tratada como o caso anterior.

Tokenizar a entrada de exemplo
Figura 10: tokenização da entrada de exemplo

Algoritmo de construção em árvore

Quando o analisador é criado, o objeto Document é criado. Durante a etapa de construção da árvore, a árvore DOM com o Documento na raiz será modificada e elementos serão adicionados a ela. Cada nó emitido pelo tokenizador será processado pelo construtor da árvore. Para cada token, a especificação define qual elemento DOM é relevante e será criado para o token. O elemento é adicionado à árvore do DOM e também à pilha de elementos abertos. Essa pilha é usada para corrigir incompatibilidades em aninhamentos e tags que não foram fechadas. O algoritmo também é descrito como uma máquina de estados. Os estados são chamados de "modos de inserção".

Vamos conferir o processo de construção da árvore para a entrada de exemplo:

<html>
  <body>
    Hello world
  </body>
</html>

A entrada para a etapa de construção da árvore é uma sequência de tokens da etapa de tokenização. O primeiro é o "modo inicial". O recebimento do token "html" causa uma mudança para o modo "antes do html" e um reprocessamento do token nesse modo. Isso causará a criação do elemento HTMLHTMLElement, que será anexado ao objeto Document raiz.

O estado é alterado para "before head". O token "body" é, então, recebido. Um HTMLHeadElement será criado implicitamente, embora não tenhamos um token "head", e será adicionado à árvore.

Agora passamos para o modo "in head" e depois para "after head". O token "body" é reprocessado, um HTMLBodyElement é criado e inserido, e o modo é transferido para "in body".

Os tokens de caracteres da string "Hello world" agora são recebidos. O primeiro causa a criação e a inserção de um nó "Texto", e os outros caracteres serão anexados a esse nó.

O recebimento do token de fim do corpo causa uma transferência para o modo "pós-corpo". Agora vamos receber a tag de término do HTML, que nos leva para o modo "after after body". O recebimento do fim do token de arquivo encerra a análise.

Construção de árvore do HTML de exemplo.
Figura 11: construção da árvore do exemplo de html.

Ações quando a análise é concluída

Nessa etapa, o navegador marca o documento como interativo e começa a analisar os scripts que estão no modo "adiado", ou seja, aqueles que devem ser executados após a análise do documento. O estado do documento será definido como "complete" e um evento "load" será disparado.

Confira os algoritmos completos para tokenização e construção de árvore na especificação do HTML5.

Tolerância a erros dos navegadores

Você nunca recebe um erro "Sintaxe inválida" em uma página HTML. Os navegadores corrigem qualquer conteúdo inválido e seguem em frente.

Veja este HTML, por exemplo:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
    Really lousy HTML
  </p>
</html>

Devo ter violado um milhão de regras ("mytag" não é uma tag padrão, aninhamento incorreto dos elementos "p" e "div" e mais), mas o navegador ainda a exibe corretamente e não reclama. Portanto, grande parte do código do analisador é feito para consertar os erros do autor HTML.

O tratamento de erros é bastante consistente em navegadores, mas, por incrível que pareça, ele não fez parte das especificações de HTML. Assim como os botões favoritos e voltar/avançar, é algo que se desenvolveu nos navegadores ao longo dos anos. Existem construções HTML inválidas conhecidas repetidas em muitos sites, e os navegadores tentam corrigi-las de uma forma em conformidade com outros navegadores.

A especificação HTML5 define alguns desses requisitos. (O WebKit resume isso muito bem no comentário no início da classe analisador de HTML.)

O analisador analisa a entrada tokenizada no documento, construindo a árvore do documento. Se o documento estiver bem formado, a análise é simples.

Infelizmente, é preciso lidar com muitos documentos HTML que não são bem formados, de modo que o analisador deve ser tolerante em relação a erros.

Precisamos cuidar de pelo menos as seguintes condições de erro:

  1. O elemento que está sendo adicionado é explicitamente proibido dentro de alguma tag externa. Nesse caso, devemos fechar todas as tags até aquela que proíbe o elemento e adicioná-la depois.
  2. Não temos permissão para adicionar o elemento diretamente. Pode ser que a pessoa que está escrevendo o documento tenha esquecido uma tag no meio (ou que a tag no meio seja opcional). Esse pode ser o caso das seguintes tags: HTML HEAD BODY TBODY TR TD LI (esqueci alguma?).
  3. Queremos adicionar um elemento de bloco dentro de um elemento inline. Feche todos os elementos inline até o próximo elemento de bloco mais alto.
  4. Se isso não ajudar, feche os elementos até que tenhamos permissão para adicionar o elemento ou ignore a tag.

Vamos analisar alguns exemplos de tolerância a erros do WebKit:

</br> em vez de <br>

Alguns sites usam </br> em vez de <br>. Para ser compatível com o IE e o Firefox, o WebKit trata o WebKit como <br>.

O código:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
     reportError(MalformedBRError);
     t->beginTag = true;
}

O tratamento de erros é interno e não será apresentado ao usuário.

Uma stray table

Uma tabela perdida é uma tabela dentro de outra tabela, mas não dentro de uma célula da tabela.

Exemplo:

<table>
  <table>
    <tr><td>inner table</td></tr>
  </table>
  <tr><td>outer table</td></tr>
</table>

O WebKit mudará a hierarquia para duas tabelas irmãs:

<table>
  <tr><td>outer table</td></tr>
</table>
<table>
  <tr><td>inner table</td></tr>
</table>

O código:

if (m_inStrayTableContent && localName == tableTag)
        popBlock(tableTag);

O WebKit usa uma pilha para o conteúdo do elemento atual: ele destacará a tabela interna da pilha da tabela externa. As tabelas agora serão irmãs.

Elementos de formulário aninhados

Quando o usuário insere um formulário dentro de outro, o segundo é ignorado.

O código:

if (!m_currentFormElement) {
        m_currentFormElement = new HTMLFormElement(formTag,    m_document);
}

Uma hierarquia de tags muito profunda

O comentário fala por si.

bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{

unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
         i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
     curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}

Tags html ou body end mal posicionadas

Mais uma vez: o comentário fala por si.

if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

Portanto, autores da Web, cuidado: a menos que você queira aparecer como exemplo em um snippet de código de tolerância a erros do WebKit, escreva um HTML bem formado.

Análise CSS

Lembra dos conceitos de análise mencionados na introdução? Bem, diferente do HTML, CSS é uma gramática livre de contexto e pode ser analisada usando os tipos de analisadores descritos na introdução. Na verdade, a especificação do CSS define a gramática léxica e sintática do CSS (link em inglês).

Vejamos alguns exemplos:

A gramática lexical (vocabulário) é definida por expressões regulares para cada token:

comment   \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num       [0-9]+|[0-9]*"."[0-9]+
nonascii  [\200-\377]
nmstart   [_a-z]|{nonascii}|{escape}
nmchar    [_a-z0-9-]|{nonascii}|{escape}
name      {nmchar}+
ident     {nmstart}{nmchar}*

"ident" é a abreviação de identificador, como o nome da classe. "name" é um ID de elemento (referido por "#")

A gramática de sintaxe é descrita em BNF.

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
selector
  : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
  ;
simple_selector
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
class
  : '.' IDENT
  ;
element_name
  : IDENT | '*'
  ;
attrib
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ] ']'
  ;
pseudo
  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
  ;

Explicação:

Um conjunto de regras é esta estrutura:

div.error, a.error {
  color:red;
  font-weight:bold;
}

div.error e a.error são seletores. A parte dentro das chaves contém as regras aplicadas por esse conjunto de regras. Essa estrutura é formalmente definida nesta definição:

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;

Isso significa que um conjunto de regras é um seletor ou, opcionalmente, um número de seletores separados por uma vírgula e espaços (o "S" significa espaço em branco). Um conjunto de regras contém chaves e, dentro delas, uma declaração ou, opcionalmente, várias declarações separadas por ponto e vírgula. Os termos "declaração" e "seletor" serão definidos nas definições BNF a seguir.

Analisador de CSS do WebKit

O WebKit usa geradores de analisadores Flex e Bison para criar analisadores automaticamente a partir dos arquivos de gramática CSS. Como você se lembra da introdução sobre o analisador, o Bison cria um analisador shift-reduce ascendente. O Firefox usa um analisador descendente escrito manualmente. Em ambos os casos, cada arquivo CSS é analisado em um objeto StyleSheet. Cada objeto contém regras CSS. Os objetos de regra CSS contêm objetos seletor e declaração e outros objetos correspondentes à gramática CSS.

Analisando CSS.
Figura 12: análise de CSS

Ordem de processamento para scripts e folhas de estilo

Scripts

O modelo da Web é síncrono. Os autores esperam que os scripts sejam analisados e executados imediatamente quando o analisador atingir uma tag <script>. A análise do documento é interrompida até que o script seja executado. Se o script for externo, primeiro o recurso deverá ser buscado na rede - isso também é feito de maneira síncrona, e a análise é interrompida até que o recurso seja obtido. Este foi o modelo por muitos anos e também está especificado nas especificações HTML4 e 5. Os autores podem adicionar o atributo "adiar" a um script. Nesse caso, ele não interrompe a análise do documento e é executado após a análise. O HTML5 adiciona uma opção para marcar o script como assíncrono para que ele seja analisado e executado por uma linha de execução diferente.

Análise especulativa

O WebKit e o Firefox fazem essa otimização. Durante a execução de scripts, outra linha de execução analisa o restante do documento e descobre quais outros recursos precisam ser carregados da rede e os carrega. Dessa forma, os recursos podem ser carregados em conexões paralelas e a velocidade geral é melhorada. Observação: o analisador especulativo só analisa referências a recursos externos, como scripts externos, folhas de estilo e imagens. Ele não modifica a árvore do DOM, deixando para o analisador principal.

Folhas de estilo

As folhas de estilo, por outro lado, têm um modelo diferente. Conceitualmente parece que, como as folhas de estilo não alteram a árvore DOM, não há motivo para esperar por elas e parar a análise do documento. No entanto, há um problema com os scripts que pedem informações de estilo durante a etapa de análise do documento. Se o estilo ainda não tiver sido carregado e analisado, o script receberá respostas erradas e isso pode causar vários problemas. Parece ser um caso extremo, mas é bastante comum. O Firefox bloqueia todos os scripts quando há uma folha de estilo que ainda está sendo carregada e analisada. O WebKit bloqueia scripts somente quando eles tentam acessar certas propriedades de estilo que podem ser afetadas por folhas de estilo descarregadas.

Construção da árvore de renderização

Enquanto a árvore do DOM é construída, o navegador constrói outra árvore, a árvore de renderização. Essa árvore contém elementos visuais na ordem em que serão exibidos. É a representação visual do documento. O objetivo dessa árvore é permitir a pintura do conteúdo na ordem correta.

O Firefox chama os elementos na árvore de renderização de "frames". O WebKit usa o termo renderizador ou objeto de renderização.

Um renderizador sabe como organizar e pintar a si mesmo e seus filhos.

A classe RenderObject do WebKit, a classe base dos renderizadores, tem a seguinte definição:

class RenderObject{
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  //the DOM node
  RenderStyle* style;  // the computed style
  RenderLayer* containgLayer; //the containing z-index layer
}

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

O tipo de box é afetado pelo valor de "exibição" do atributo de estilo relevante para o nó (consulte a seção Computação de estilo). Este é o código do WebKit para decidir que tipo de renderizador deve ser criado para um nó DOM, de acordo com o atributo display:

RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
    Document* doc = node->document();
    RenderArena* arena = doc->renderArena();
    ...
    RenderObject* o = 0;

    switch (style->display()) {
        case NONE:
            break;
        case INLINE:
            o = new (arena) RenderInline(node);
            break;
        case BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case INLINE_BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case LIST_ITEM:
            o = new (arena) RenderListItem(node);
            break;
       ...
    }

    return o;
}

O tipo de elemento também é considerado: por exemplo, controles de formulário e tabelas têm molduras especiais.

No WebKit, se um elemento quiser criar um renderizador especial, ele vai substituir o método createRenderer(). Os renderizadores apontam para objetos de estilo que contêm informações não geométricas.

A relação da árvore de renderização com a árvore DOM

Os renderizadores correspondem a elementos DOM, mas a relação não é de um para um. Elementos DOM não visuais não serão inseridos na árvore de renderização. Um exemplo é o elemento "head". Além disso, os elementos cujo valor de exibição foi atribuído a "none" não aparecerão na árvore. Os elementos com visibilidade "oculto" aparecerão na árvore.

Existem elementos DOM que correspondem a vários objetos visuais. Geralmente, são elementos com estrutura complexa que não podem ser descritos por um único retângulo. Por exemplo, o elemento "select" tem três renderizadores: um para a área de exibição, um para a caixa de listagem suspensa e outro para o botão. Além disso, quando o texto é dividido em várias linhas porque a largura não é suficiente para uma linha, as novas linhas são adicionadas como renderizadores extras.

Outro exemplo de vários renderizadores é HTML corrompido. De acordo com as especificações de CSS, um elemento in-line deve conter apenas elementos de bloco ou somente elementos in-line. No caso de conteúdo misto, renderizadores de blocos anônimos são criados para unir os elementos inline.

Alguns objetos de renderização correspondem a um nó DOM, mas não no mesmo local da árvore. Floats e elementos posicionados de forma absoluta estão fora do fluxo, posicionados em uma parte diferente da árvore e mapeados para o frame real. Um frame de espaço reservado é onde eles deveriam estar.

A árvore de renderização e a árvore DOM correspondente.
Figura 13: a árvore de renderização e a árvore DOM correspondente. A "janela de visualização" é o bloco que contém o conteúdo inicial. No WebKit, será o objeto "RenderView".

O fluxo de construção da árvore

No Firefox, a apresentação é registrada como um listener para atualizações do DOM. A apresentação delega a criação de frames ao FrameConstructor, e o construtor resolve o estilo (consulte a computação de estilo) e cria um frame.

No WebKit, o processo de resolver o estilo e criar um renderizador é chamado de "attachment". Cada nó DOM tem um método "attach". O anexo é síncrono, a inserção de nós na árvore DOM chama o novo método "attach" do nó.

O processamento das tags html e body resulta na construção da raiz da árvore de renderização. O objeto raiz de renderização corresponde ao que a especificação CSS chama de bloco que o contém: o bloco superior que contém todos os outros blocos. Suas dimensões são a janela de visualização: as dimensões da área de exibição da janela do navegador. O Firefox a chama de ViewPortFrame e o WebKit chama de RenderView. Este é o objeto de renderização para o qual o documento aponta. O restante da árvore é construído como uma inserção de nós DOM.

Consulte as especificações do CSS2 sobre o modelo de processamento.

Computação de estilo

A construção da árvore de renderização requer o cálculo das propriedades visuais de cada objeto de renderização. Isso é feito pelo cálculo das propriedades de estilo de cada elemento.

O estilo inclui folhas de estilo de várias origens, elementos de estilo in-line e propriedades visuais no HTML (como a propriedade "bgcolor").Esta é traduzida para as propriedades de estilo CSS correspondentes.

A origem das folhas de estilo são as folhas de estilo padrão do navegador, as folhas de estilo fornecidas pelo autor da página e as folhas de estilo do usuário. Elas são folhas de estilo fornecidas pelo usuário do navegador (os navegadores permitem que você defina seus estilos favoritos. No Firefox, por exemplo, isso é feito colocando uma folha de estilo na pasta "Perfil Firefox").

A computação de estilo traz algumas dificuldades:

  1. Dados de estilo são uma construção muito grande que contém inúmeras propriedades de estilo. Isso pode causar problemas de memória.
  2. Encontrar as regras correspondentes para cada elemento pode causar problemas de desempenho se ele não estiver otimizado. Analisar toda a lista de regras de cada elemento para encontrar correspondências é uma tarefa pesada. Os seletores podem ter uma estrutura complexa que pode fazer com que o processo de correspondência comece por um caminho aparentemente promissor que se mostrou inútil, e outro caminho precisa ser tentado.

    Por exemplo, este seletor composto:

    div div div div{
    ...
    }
    

    Isso significa que as regras se aplicam a <div> que é descendente de 3 divs. Suponha que você queira verificar se a regra se aplica a um determinado elemento <div>. Você escolhe um determinado caminho pela árvore para verificação. Talvez seja necessário percorrer a árvore de nós para descobrir que há apenas dois divs e que a regra não se aplica. Depois, você precisa tentar outros caminhos na árvore.

  3. A aplicação das regras envolve regras em cascata complexas que definem a hierarquia das regras.

Vejamos como os navegadores enfrentam esses problemas:

Compartilhando dados de estilo

Os nós do WebKit fazem referência a objetos de estilo (RenderStyle). Esses objetos podem ser compartilhados por nós em algumas condições. Os nós são irmãos ou primos e:

  1. Os elementos devem estar no mesmo estado do mouse (por exemplo, um não pode estar no :hover enquanto o outro não está)
  2. Nenhum elemento deve ter um ID
  3. Os nomes das tags devem ser correspondentes
  4. Os atributos de classe precisam corresponder
  5. O conjunto de atributos mapeados precisa ser idêntico
  6. Os estados de links precisam corresponder
  7. Os estados de foco precisam corresponder
  8. Nenhum elemento deve ser afetado por seletores de atributo, em que o problema é definido como tendo qualquer correspondência de seletor que use um seletor de atributo em qualquer posição dentro do seletor
  9. Não pode haver atributo de estilo in-line nos elementos
  10. Não pode haver seletores irmãos em uso. O WebCore simplesmente ativa uma mudança global quando qualquer seletor irmão é encontrado e desativa o compartilhamento de estilo para todo o documento quando eles estão presentes. Isso inclui o seletor + e outros como :first-child e :last-child.

Árvore de regras do Firefox

O Firefox tem duas árvores extras para facilitar o cálculo do estilo: a árvore de regras e a árvore de contexto de estilo. O WebKit também possui objetos de estilo, mas eles não são armazenados em uma árvore como a árvore de contexto de estilo. Somente o nó DOM aponta para o estilo relevante.

Árvore de contexto no estilo do Firefox.
Figura 14: árvore de contexto no estilo Firefox.

Os contextos de estilo contêm valores finais. Os valores são calculados aplicando todas as regras correspondentes na ordem correta e realizando manipulações que os transformam de valores lógicos em valores concretos. Por exemplo, se o valor lógico for uma porcentagem da tela, ele será calculado e transformado em unidades absolutas. A ideia da árvore de regras é realmente inteligente. Ele permite o compartilhamento desses valores entre os nós para evitar calculá-los novamente. Isso também economiza espaço.

Todas as regras correspondentes são armazenadas em uma árvore. Os nós inferiores em um caminho têm prioridade mais alta. A árvore contém todos os caminhos das correspondências de regra encontradas. O armazenamento dessas regras é feito lentamente. A árvore não é calculada no início para cada nó, mas sempre que um estilo de nó precisa ser calculado, os caminhos calculados são adicionados à árvore.

A ideia é ver os caminhos das árvores como palavras em um léxico. Digamos que já tenhamos computado esta árvore de regras:

Árvore de regras calculada
Figura 15: árvore de regras calculada.

Suponha que precisamos fazer a correspondência das regras para outro elemento na árvore de conteúdo e descobrir que as regras correspondentes (na ordem correta) são B-E-I. Já temos esse caminho na árvore porque já calculamos o caminho A-B-E-I-L. Agora temos menos trabalho a fazer.

Vamos ver como a árvore economiza nosso trabalho.

Divisão em structs

Os contextos de estilo são divididos em structs. Essas estruturas contêm informações de estilo para uma determinada categoria, como borda ou cor. Todas as propriedades em uma estrutura são herdadas ou não herdadas. Propriedades herdadas são propriedades que, a menos que definidas pelo elemento, são herdadas do pai. Propriedades não herdadas (chamadas de propriedades "redefinidas") usam valores padrão se não forem definidas.

Ela nos ajuda a armazenar structs inteiras em cache (contendo os valores finais computados) na árvore. A ideia é que, se o nó inferior não fornecer uma definição para uma estrutura, uma estrutura armazenada em cache em um nó superior poderá ser usada.

Computação de contextos de estilo usando a árvore de regras

Ao computar o contexto de estilo de um determinado elemento, primeiro computamos um caminho na árvore de regras ou usamos um já existente. Em seguida, começamos a aplicar as regras no caminho para preencher as estruturas no novo contexto de estilo. Começamos pelo nó inferior do caminho - aquele com a precedência mais alta (geralmente o seletor mais específico) e atravessamos a árvore até que nossa estrutura esteja completa. Se não houver especificação para o struct nesse nó de regra, poderemos otimizar bastante. Vamos subir na árvore até encontrar um nó que o especifique totalmente e aponte para ele. Essa é a melhor otimização. O struct inteiro é compartilhado. Isso economiza a computação de valores finais e memória.

Se encontrarmos definições parciais, vamos subir na árvore até que a estrutura esteja preenchida.

Se não encontramos nenhuma definição para o nosso struct, caso ele seja do tipo "herdado", vamos apontar para o struct do pai na árvore de contexto. Nesse caso, também conseguimos compartilhar structs. Se for um struct redefinido, os valores padrão serão usados.

Se o nó mais específico adicionar valores, precisaremos fazer alguns cálculos extras para transformá-lo em valores reais. Armazenamos o resultado em cache no nó da árvore para que ele possa ser usado pelos filhos.

Caso um elemento tenha um irmão ou irmão que aponte para o mesmo nó de árvore, todo o contexto do estilo pode ser compartilhado entre eles.

Vamos ver um exemplo: Suponha que temos um código HTML

<html>
  <body>
    <div class="err" id="div1">
      <p>
        this is a <span class="big"> big error </span>
        this is also a
        <span class="big"> very  big  error</span> error
      </p>
    </div>
    <div class="err" id="div2">another error</div>
  </body>
</html>

E as seguintes regras:

div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}

Para simplificar as coisas, digamos que precisamos preencher apenas dois structs: a estrutura de cor e a struct de margem. A estrutura de cor contém apenas um membro: a cor. A estrutura de margem contém os quatro lados.

A árvore de regras resultante será semelhante a esta (os nós são marcados com o nome do nó: o número da regra para a qual apontam):

A árvore de regras
Figura 16: a árvore de regras

A árvore de contexto ficará assim (nome do nó: nó da regra para a qual apontam):

A árvore de contexto.
Figura 17: a árvore de contexto

Suponha que analisemos o HTML e cheguemos à segunda tag <div>. Precisamos criar um contexto de estilo para esse nó e preencher as estruturas de estilo dele.

Vamos fazer a correspondência das regras e descobrir que as regras correspondentes para <div> são 1, 2 e 6. Isso significa que já existe um caminho na árvore que nosso elemento pode usar e só precisamos adicionar outro nó a ele para a regra 6 (nó F na árvore de regras).

Vamos criar um contexto de estilo e colocá-lo na árvore de contexto. O novo contexto de estilo apontará para o nó F na árvore de regras.

Agora precisamos preencher as estruturas de estilo. Vamos começar preenchendo a estrutura de margem. Como o último nó de regra (F) não é adicionado à estrutura da margem, podemos subir na árvore até encontrar e usar uma struct em cache computada em uma inserção de nó anterior. Nós a encontraremos no nó B, que é o nó superior entre os que especificam regras de margem.

Como temos uma definição para a estrutura de cor, não podemos usar um struct armazenado em cache. Como a cor tem um atributo, não precisamos subir na árvore para preencher outros atributos. Calcularemos o valor final (converter a string para RGB etc.) e armazenaremos em cache a struct calculada nesse nó.

O trabalho no segundo elemento <span> é ainda mais fácil. Vamos combinar as regras e chegar à conclusão de que ele aponta para a regra G, como o período anterior. Como temos irmãos que apontam para o mesmo nó, podemos compartilhar todo o contexto do estilo e apenas apontar para o contexto do período anterior.

Para estruturas que contêm regras herdadas do pai, o armazenamento em cache é feito na árvore de contexto (a propriedade de cor é, na verdade, herdada, mas o Firefox a trata como redefinida e a armazena em cache na árvore de regras).

Por exemplo, se adicionássemos regras para fontes em um parágrafo:

p {font-family: Verdana; font size: 10px; font-weight: bold}

Então o elemento de parágrafo, que é filho do div na árvore de contexto, poderia ter compartilhado a estrutura de fonte do mesmo pai. Isso ocorre se nenhuma regra de fonte tiver sido especificada para o parágrafo.

No WebKit, que não possui uma árvore de regras, as declarações correspondentes são transferidas quatro vezes. Primeiro as propriedades não importantes de alta prioridade são aplicadas (propriedades que devem ser aplicadas primeiro porque outras dependem delas, como exibição), depois, alta prioridade são importantes, depois não importantes de prioridade normal e, por fim, regras importantes com prioridade normal. Isso significa que as propriedades que aparecem várias vezes são resolvidas de acordo com a ordem em cascata correta. Os últimos vencem.

Resumindo: compartilhar os objetos de estilo (inteiramente ou alguns dos structs dentro deles) resolve os problemas 1 e 3. A árvore de regras do Firefox também ajuda a aplicar as propriedades na ordem correta.

Manipulação das regras para uma fácil correspondência

Existem várias fontes para regras de estilo:

  1. Regras CSS, seja em folhas de estilo externas ou em elementos de estilo. css p {color: blue}
  2. Atributos de estilo inline, como html <p style="color: blue" />
  3. Atributos visuais HTML (mapeados para regras de estilo relevantes) html <p bgcolor="blue" /> Os dois últimos são facilmente associados ao elemento, já que ele é proprietário dos atributos de estilo, e os atributos HTML podem ser mapeados usando o elemento como chave.

Como observado no problema no 2, a correspondência de regras CSS pode ser mais complicada. Para resolver a dificuldade, as regras são manipuladas para facilitar o acesso.

Após analisar a folha de estilo, as regras são adicionadas a um dos vários mapas hash, de acordo com o seletor. Há mapas por ID, nome de classe, nome de tag e um mapa geral para tudo que não se encaixa nessas categorias. Se o seletor for um ID, a regra será adicionada ao mapa de IDs; se for uma classe, ela será adicionada ao mapa de classes etc.

Essa manipulação facilita muito a correspondência de regras. Não é preciso verificar cada declaração: podemos extrair dos mapas as regras relevantes para um elemento. Essa otimização elimina mais de 95% das regras, ou seja, elas não precisam ser consideradas durante o processo de correspondência(4.1).

Vejamos, por exemplo, as seguintes regras de estilo:

p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}

A primeira regra será inserida no mapa da turma. O segundo no mapa de IDs e o terceiro no mapa de tags.

Para o seguinte fragmento HTML:

<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>

Tentaremos primeiro encontrar regras para o elemento p. O mapa de classe conterá uma chave "error" sob a qual a regra "p.error" é encontrada. O elemento div terá regras relevantes no mapa de ID (a chave é o ID) e no mapa de tags. Portanto, o único trabalho que resta é descobrir qual das regras extraídas pelas chaves realmente corresponde.

Por exemplo, se a regra para a div for:

table div {margin: 5px}

Ele ainda será extraído do mapa de tags, porque a chave é o seletor mais à direita, mas não corresponderia ao nosso elemento div, que não tem um ancestral de tabela.

O WebKit e o Firefox fazem essa manipulação.

Ordem em cascata da folha de estilo

O objeto de estilo tem propriedades correspondentes a todos os atributos visuais (todos os atributos CSS, mas é mais genérico). Se a propriedade não for definida por nenhuma das regras correspondentes, algumas propriedades poderão ser herdadas pelo objeto de estilo do elemento pai. Outras propriedades têm valores padrão.

O problema começa quando há mais de uma definição. Aí vem a ordem em cascata para resolver a questão.

Uma declaração para uma propriedade de estilo pode aparecer em várias folhas de estilo e várias vezes em uma folha de estilo. Isso significa que a ordem de aplicação das regras é muito importante. Isso é chamado de ordem em cascata. De acordo com a especificação CSS2, a ordem em cascata é (do menor para o maior):

  1. Declarações do navegador
  2. Declarações normais do usuário
  3. Declarações normais do autor
  4. Crie declarações importantes
  5. Declarações importantes do usuário

As declarações do navegador são menos importantes e o usuário se sobrepõe ao autor somente se a declaração tiver sido marcada como importante. As declarações com a mesma ordem são classificadas por especificidade e depois pela ordem em que foram especificadas. Os atributos visuais HTML são convertidos em declarações CSS correspondentes . Elas são tratadas como regras de autor com baixa prioridade.

Especificidade

A especificidade do seletor é definida pela especificação CSS2 da seguinte forma:

  1. conte 1 se a declaração de origem for um atributo de "estilo" e não uma regra com um seletor; caso contrário, (= a)
  2. conte o número de atributos de ID no seletor (= b)
  3. contar o número de outros atributos e pseudoclasses no seletor (= c)
  4. contar o número de nomes de elementos e pseudoelementos no seletor (= d)

A concatenação dos quatro números a-b-c-d (em um sistema numérico com base grande) gera a especificidade.

A base numérica que você precisa usar é definida pela maior contagem possível em uma das categorias.

Por exemplo, se a=14, é possível usar a base hexadecimal. No caso improvável de a=17, você precisará de uma base numérica de 17 dígitos. Isso pode acontecer com um seletor como este: html body div div p... (17 tags no seu seletor... pouco provável).

Alguns exemplos:

 *             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

Como classificar as regras

Após a correspondência das regras, elas são classificadas de acordo com as regras em cascata. O WebKit usa a classificação em balão para listas pequenas e a classificação em mescla para as grandes. O WebKit implementa a classificação substituindo o operador > para as regras:

static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
    int spec1 = r1.selector()->specificity();
    int spec2 = r2.selector()->specificity();
    return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
}

Processo gradual

O WebKit usa um sinalizador que marca se todas as folhas de estilo de nível superior (incluindo @imports) foram carregadas. Se o estilo não estiver totalmente carregado durante a anexação, os marcadores serão utilizados e ele será marcado no documento, e eles serão recalculados quando as folhas de estilo forem carregadas.

Layout

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

O HTML usa um modelo de layout baseado em fluxo, o que significa que, na maioria das vezes, é possível computar a geometria em uma única passagem. Elementos posteriores "no fluxo" normalmente não afetam a geometria dos elementos que estão anteriormente "no fluxo", então o layout pode prosseguir da esquerda para a direita, de cima para baixo no documento. Há exceções: por exemplo, tabelas HTML podem exigir mais de um cartão.

O sistema de coordenadas é relativo ao frame raiz. As coordenadas superior e esquerda são usadas.

O layout é um processo recursivo. Ele começa no renderizador raiz, que corresponde ao elemento <html> do documento HTML. O layout continua recursivamente por toda ou parte da hierarquia de frames, calculando informações geométricas para cada renderizador que precisa delas.

A posição do renderizador raiz é 0,0 e suas dimensões são a janela de visualização, a parte visível da janela do navegador.

Todos os renderizadores têm um método de “layout” ou “reflow”, cada um invocando o método de layout dos filhos que precisam de layout.

Sistema de bits incorretos

Para não fazer um layout completo a cada pequena mudança, os navegadores usam um sistema de "bits sujos". Um renderizador que é alterado ou adicionado marca a si mesmo e seus filhos como "sujos": precisando de layout.

Há dois sinalizadores: "sujo" e "filhos estão sujos", o que significa que, embora o próprio renderizador possa estar OK, ele tem pelo menos um filho que precisa de um layout.

Layout global e incremental

O layout pode ser acionado em toda a árvore de renderização. Este layout é "global". Isso pode acontecer como resultado de:

  1. Uma mudança de estilo global que afeta todos os renderizadores, como uma mudança de tamanho de fonte.
  2. Como resultado do redimensionamento de uma tela.

O layout pode ser incremental, mas apenas os renderizadores incorretos serão dispostos (isso pode causar alguns danos que exigem layouts adicionais).

O layout incremental é acionado (de forma assíncrona) quando os renderizadores estão sujos. Por exemplo, quando novos renderizadores são anexados à árvore de renderização depois que conteúdo extra veio da rede e foi adicionado à árvore DOM.

Layout incremental.
Figura 18: layout incremental - somente renderizadores incorretos e seus filhos são exibidos

Layout assíncrono e síncrono

O layout incremental é feito de forma assíncrona. O Firefox coloca na fila "comandos de reflow" para layouts incrementais e um programador aciona a execução em lote desses comandos. O WebKit também tem um timer que executa um layout incremental - a árvore é percorrida e os renderizadores "sujos" saem do layout.

Scripts que solicitam informações de estilo, como "offsetHeight", podem acionar o layout incremental de forma síncrona.

O layout global geralmente será acionado de forma síncrona.

Às vezes, o layout é acionado como um callback após um layout inicial porque alguns atributos, como a posição de rolagem, mudam.

Otimizações

Quando um layout é acionado por um "redimensionamento" ou mudança na posição do renderizador(e não no tamanho), os tamanhos das renderizações são retirados de um cache e não são recalculados...

Em alguns casos, apenas uma subárvore é modificada e o layout não começa na raiz. Isso pode acontecer nos casos em que a mudança é local e não afeta os arredores, como um texto inserido em campos de texto. Caso contrário, cada tecla pressionada acionaria um layout começando na raiz.

O processo de layout

O layout geralmente tem o seguinte padrão:

  1. O renderizador pai determina a própria largura.
  2. O familiar responsável fala sobre os filhos e:
    1. Posiciona o renderizador filho (define seu x e y).
    2. Se necessário, chama o layout filho (sujo ou estamos em um layout global ou, por algum outro motivo), que calcula a altura do filho.
  3. O pai usa as alturas cumulativas dos filhos e as alturas das margens e do preenchimento para definir sua própria altura, que será usada pelo pai do renderizador pai.
  4. Define o bit sujo como falso.

O Firefox usa um objeto "estado" (nsHTMLReflowState) como parâmetro para o layout (chamado de "reflow"). Entre outras, o estado inclui a largura dos pais.

A saída do layout do Firefox é um objeto "metrics"(nsHTMLReflowMetrics). Ele contém a altura calculada pelo renderizador.

Cálculo da largura

A largura do renderizador é calculada usando a largura do bloco do contêiner, a propriedade "width" de estilo do renderizador, as margens e bordas.

Por exemplo, a largura da seguinte div:

<div style="width: 30%"/>

Seria calculada pelo WebKit da seguinte maneira(classe RenderBox método calcWidth):

  • A largura do contêiner é o máximo da availableWidth de contêineres e 0. A availableWidth neste caso é contentWidth, calculada da seguinte forma:
clientWidth() - paddingLeft() - paddingRight()

clientWidth e clientHeight representam o interior de um objeto, excluindo a borda e a barra de rolagem.

  • A largura dos elementos é o atributo de estilo "width". Ela será calculada como um valor absoluto pela computação da porcentagem da largura do contêiner.

  • As bordas horizontais e paddings foram adicionados.

Até agora, esse era o cálculo da "largura preferencial". Agora as larguras mínima e máxima serão calculadas.

Se a largura preferencial for maior que a largura máxima, a largura máxima será usada. Se for menor que a largura mínima (a menor unidade inquebrável), a largura mínima será usada.

Os valores são armazenados em cache caso um layout seja necessário, mas a largura não muda.

Quebra de linha

Quando um renderizador no meio de um layout decide que precisa ser quebrado, o renderizador para e propaga para o pai do layout que ele precisa ser corrompido. O pai cria outros renderizadores e chama o layout neles.

Pintura

No estágio de pintura, a árvore de renderização é percorrida e o método "paint()" do renderizador é chamado para exibir o conteúdo na tela. A pintura usa o componente de infraestrutura da IU.

Global e incremental

Assim como o layout, a pintura também pode ser global (toda a árvore é pintada) ou incremental. Na pintura incremental, alguns renderizadores mudam de forma que não afeta toda a árvore. O renderizador alterado invalida o retângulo na tela. Isso faz com que o SO a veja como uma "região suja" e gere um evento "pintura". O SO faz isso de forma inteligente e reúne várias regiões em uma. No Chrome, isso é mais complicado porque o renderizador está em um processo diferente do principal. O Chrome simula o comportamento do SO até certo ponto. A apresentação detecta esses eventos e delega a mensagem à raiz de renderização. A árvore é percorrida até que o renderizador relevante seja alcançado. Ele pintará a si mesmo (e geralmente aos filhos).

A ordem de pintura

O CSS2 define a ordem do processo de pintura. Na verdade, essa é a ordem em que os elementos são empilhados nos contextos de empilhamento. Essa ordem afeta a pintura, uma vez que as pilhas são pintadas de trás para a frente. A ordem de empilhamento de um renderizador em bloco é:

  1. background color
  2. imagem de plano de fundo
  3. border
  4. crianças
  5. outline

Lista de exibição do Firefox

O Firefox repassa a árvore de renderização e cria uma lista de exibição para o retângulo pintado. Contém os renderizadores relevantes para o retângulo, na ordem correta de pintura (planos de fundo dos renderizadores, depois bordas etc.).

Dessa forma, a árvore precisa ser percorrida apenas uma vez para pintar, e não várias vezes, pintando todos os planos de fundo, depois todas as imagens, depois todas as bordas etc.

O Firefox otimiza o processo não adicionando elementos que ficarão ocultos, como elementos completamente sob outros elementos opacos.

Armazenamento em retângulo do WebKit

Antes de repintar, o WebKit salva o retângulo antigo como um bitmap. Em seguida, pinta apenas o delta entre os retângulos novo e antigo.

Alterações dinâmicas

Os navegadores tentam fazer o mínimo possível de ações em resposta a uma mudança. Portanto, as mudanças na cor de um elemento causarão apenas a nova pintura desse elemento. Mudanças na posição do elemento causam o layout e a nova pintura do elemento, dos filhos e, possivelmente, dos irmãos dele. A adição de um nó DOM causa o layout e a nova pintura do nó. Mudanças significativas, como o aumento do tamanho da fonte no elemento "html", causam a invalidação de caches, um novo layout e a nova pintura de toda a árvore.

As linhas de execução do mecanismo de renderização

O mecanismo de renderização tem um único thread. Quase tudo, exceto operações de rede, acontece em um único encadeamento. No Firefox e no Safari, essa é a principal thread do navegador. No Chrome, é a linha de execução principal do processo de guia.

As operações de rede podem ser realizadas por várias linhas de execução paralelas. O número de conexões paralelas é limitado (geralmente de 2 a 6 conexões).

Loop de eventos

A linha de execução principal do navegador é um loop de eventos. É um loop infinito que mantém o processo ativo. Ele aguarda eventos (como eventos de layout e pintura) e os processa. Este é o código do Firefox para o loop de eventos principal:

while (!mExiting)
    NS_ProcessNextEvent(thread);

Modelo visual CSS2

A tela

De acordo com a especificação CSS2 (em inglês), o termo "canvas" descreve "o espaço em que a estrutura de formatação é renderizada": onde o navegador pinta o conteúdo.

A tela é infinita para cada dimensão do espaço, mas os navegadores escolhem uma largura inicial com base nas dimensões da janela de visualização.

De acordo com www.w3.org/TR/CSS2/zindex.html (link em inglês), a tela será transparente se contida em outra ou, caso contrário, terá uma cor definida pelo navegador.

Modelo de box CSS

O modelo de box CSS descreve as caixas retangulares geradas para elementos na árvore do documento e dispostas de acordo com o modelo de formatação visual.

Cada caixa tem uma área de conteúdo (por exemplo, texto, uma imagem etc.) e áreas de margem, bordas e padding opcionais.

Modelo de box CSS2
Figura 19: modelo de caixa CSS2

Cada nó gera 0...n caixas desse tipo.

Todos os elementos têm uma propriedade "display" que determina o tipo de box que será gerado.

Exemplos:

block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.

O padrão é inline, mas a folha de estilos do navegador pode definir outros padrões. Por exemplo: a exibição padrão do elemento "div" é bloco.

Um exemplo de folha de estilo padrão está disponível aqui: www.w3.org/TR/CSS2/sample.html (link em inglês).

Esquema de posicionamento

Há três esquemas:

  1. Normal: o objeto é posicionado de acordo com seu lugar no documento. Isso significa que seu lugar na árvore de renderização é como o lugar na árvore DOM e disposto de acordo com o tipo e as dimensões de caixa
  2. Flutuante: primeiro o objeto é disposto como fluxo normal e depois é movido para a esquerda ou direita o máximo possível.
  3. Absoluto: o objeto é colocado na árvore de renderização em um local diferente da árvore DOM.

O esquema de posicionamento é definido pela propriedade "position" e pelo atributo "float".

  • estático e relativo geram um fluxo normal
  • posicionamento absoluto e fixo de causa

No posicionamento estático, nenhuma posição é definida, e o posicionamento padrão é usado. Em outros esquemas, o autor especifica a posição: superior, inferior, esquerda, direita.

A maneira como a caixa está disposta é determinada por:

  • Tipo de box
  • Dimensões da caixa
  • Esquema de posicionamento
  • Informações externas, como tamanho da imagem e da tela

Tipos de box

Caixa em bloco: forma um bloco, tem seu próprio retângulo na janela do navegador.

Caixa de bloco.
Figura 20: box em bloco

Box in-line: não tem seu próprio bloco, mas está dentro de um bloco que a contém.

Boxes inline.
Figura 21: caixas inline

Os blocos são formatados verticalmente um após o outro. In-line são formatados horizontalmente.

Formatação em bloco e inline.
Figura 22: formatação em bloco e inline

Boxes inline são colocados dentro de linhas ou "caixas de linha". As linhas têm pelo menos a mesma altura da caixa mais alta, mas podem ser mais altas quando as caixas estão alinhadas como "linha de base", ou seja, a parte inferior de um elemento está alinhada em um ponto de outra caixa que não a parte inferior. Se a largura do contêiner não for suficiente, as in-line serão dispostas em várias linhas. Isso geralmente é o que acontece em um parágrafo.

Linhas.
Figura 23: linhas

Posicionamento

Relativo

Posicionamento relativo - posicionado como de costume e movido pelo delta necessário.

Posicionamento relativo.
Figura 24: posicionamento relativo

Variações

Uma caixa flutuante é movida para a esquerda ou direita de uma linha. Uma característica interessante é que as outras caixas fluem em torno dele. O HTML:

<p>
  <img style="float: right" src="images/image.gif" width="100" height="100">
  Lorem ipsum dolor sit amet, consectetuer...
</p>

Será semelhante a:

Ponto flutuante.
Figura 25: ponto flutuante

Absoluto e fixo

O layout é definido exatamente, independentemente do fluxo normal. O elemento não participa do fluxo normal. As dimensões são relativas ao contêiner. Em "fixo", o contêiner é a janela de visualização.

Posicionamento fixo.
Figura 26: posicionamento fixo

Representação em camadas

Isso é especificado pela propriedade CSS z-index. Ele representa a terceira dimensão do box: sua posição ao longo do "eixo z".

Os box são divididos em pilhas (chamadas de pilhas de contexto). Em cada pilha, os elementos de trás são pintados primeiro e os elementos da frente são pintados primeiro, mais perto do usuário. Em caso de sobreposição, o elemento principal ocultará o anterior.

As pilhas são ordenadas de acordo com a propriedade Z-index. Boxes com propriedade "z-index" formam uma pilha local. A janela de visualização tem a pilha externa.

Exemplo:

<style type="text/css">
  div {
    position: absolute;
    left: 2in;
    top: 2in;
  }
</style>

<p>
  <div
    style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
  </div>
  <div
    style="z-index: 1;background-color:green;width: 2in; height: 2in;">
  </div>
</p>

O resultado será este:

Posicionamento fixo.
Figura 27: posicionamento fixo

Embora o div vermelho preceda o verde na marcação e tenha sido pintada antes no fluxo normal, a propriedade z-index é maior, por isso está mais adiante na pilha mantida pela caixa raiz.

Recursos

  1. Arquitetura do navegador

    1. Grosskurth, Alan. Uma arquitetura de referência para navegadores da Web (pdf)
    2. Gupta, Vineet. Como os navegadores funcionam - Parte 1: arquitetura
  2. Análise

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (também conhecido como "Dragon book"), Addison-Wesley, 1986
    2. Rick Jelliffe. Os belos e o negrito: dois novos rascunhos para HTML 5.
  3. Firefox

    1. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (vídeo do Google Tech Talk)
    3. L. David Baron, Layout Engine do Mozilla
    4. L. David Baron, Mozilla Style System Documentation
    5. Chris Waterson, Notes on HTML Reflow (em inglês)
    6. Chris Waterson, Gecko Overview
    7. Alexander Larsson, The life of an HTML HTTP request (em inglês)
  4. WebKit

    1. David Hyatt, Como implementar CSS(parte 1)
    2. David Hyatt, Uma visão geral do WebCore
    3. David Hyatt, Renderização do WebCore
    4. David Hyatt, O problema do FOUC
  5. Especificações W3C

    1. Especificação de HTML 4.01
    2. Especificação de HTML5 do W3C
    3. Especificação das folhas de estilo em cascata: nível 2, revisão 1 (CSS 2.1)
  6. Instruções de criação dos navegadores

    1. Firefox. https://developer.mozilla.org/Build_Documentation (link em inglês)
    2. WebKit. http://webkit.org/building/build.html (link em inglês)

Traduções

Esta página foi traduzida para o japonês duas vezes:

É possível visualizar as traduções hospedadas externamente de coreano e turco.

Agradecemos a todos!