Como os navegadores funcionam

Nos bastidores dos navegadores da Web modernos

Prefácio

Esta introdução abrangente sobre as operações internas do WebKit e do Gecko é a resultado de muita pesquisa feita pela desenvolvedora israelense Tali Garsiel. Mais de alguns anos, ela analisou todos os dados publicados sobre o funcionamento interno do navegador e passou um muito tempo lendo o código-fonte do navegador. Ela escreveu:

Como desenvolvedor Web, aprender os aspectos internos das operações de um navegador ajuda você a tomar decisões melhores e conhecer as justificativas por trás do desenvolvimento práticas recomendadas. Embora este seja um documento um pouco longo, recomendamos você passa um tempo se aprofundando. Você vai gostar do resultado.

Paul Ireland, Relações com desenvolvedores do Chrome

Introdução

Os navegadores da Web são os softwares mais amplamente utilizados. Nesta introdução, explico e trabalham 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 sobre os quais falaremos

Atualmente, existem cinco principais navegadores usados em computadores: Chrome, Internet Explorer, Firefox, Safari e Opera. Nos celulares, os principais navegadores são o Navegador do Android, iPhone, Opera Mini e Opera Mobile, UC Browser, os navegadores Nokia S40/S60 e Chrome. Todos eles, exceto os navegadores Opera, são baseados no WebKit. Darei exemplos dos navegadores de código aberto Firefox e Chrome e Safari (que é parcialmente de código aberto). De acordo com as estatísticas da StatCounter (em inglês) de 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 Android, o iPhone e o Chrome constituem cerca de 54% do uso.

A principal funcionalidade do navegador

A principal função de um navegador é apresentar o recurso da web que você escolhe, 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 usando um URI (Identificador de Recurso Uniforme).

A forma como o navegador interpreta e exibe arquivos HTML é definida nas especificações de HTML e CSS. Essas especificações são mantidas pelo W3C (Consórcio da World Wide Web), a organização que controla os padrões para a Web. Por anos, os navegadores mantiveram-se de acordo com apenas uma parte das especificações e desenvolveram suas 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 do usuário dos navegadores têm muito em comum. Entre os elementos comuns da 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 atualizar e parar para atualizar ou interromper o carregamento de documentos atuais
  5. Botão "Página inicial" que leva você para a página inicial

Curiosamente, a interface do usuário do navegador não está em uma especificação formal. Ela é resultado de boas práticas moldadas ao longo de anos de experiência e da imitação de navegadores. A especificação HTML5 não define os elementos de IU que um navegador precisa ter, mas lista alguns elementos comuns. Entre eles estão a barra de endereço, a barra de status e a barra de ferramentas. Obviamente, há 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. A 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. Mecanismo do navegador: organiza as ações entre a interface e o mecanismo de renderização.
  3. O mecanismo de renderização: responsável por exibir o conteúdo solicitado. Por exemplo, se o conteúdo solicitado for HTML, o mecanismo de renderização analisa HTML e CSS e exibe o conteúdo analisado na tela.
  4. Rede: para chamadas de rede, como solicitações HTTP, com o uso de diferentes implementações para cada plataforma por trás de uma interface independente.
  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 da plataforma. Abaixo, ele usa métodos de interface do usuário do sistema operacional.
  6. Intérprete de JavaScript. Usado para analisar e executar 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 são compatíveis com 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, a exibição de documentos PDF usando um plug-in de visualizador de PDF. No entanto, neste capítulo, vamos nos concentrar no caso de uso principal: exibição de HTML e imagens formatadas com CSS.

Navegadores diferentes usam mecanismos de renderização diferentes: o Internet Explorer usa o Trident, o Firefox usa o Gecko e o Safari usa o WebKit. O Chrome e o Opera (a partir da versão 15) usam o Blink, uma ramificação 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.

O fluxo principal

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

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

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

O mecanismo de renderização começará a analisar o documento HTML e converterá os 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 em elementos de estilo. A combinação de informações de estilo com instruções visuais no HTML será usada 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 certa para serem mostrados na tela.

Após a construção da árvore de renderização, ela passa por um layout de desenvolvimento de software. Isso significa dar a cada nó as coordenadas exatas de onde 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 tentará exibir o conteúdo na tela o mais rápido possível. Ele não espera até que todo o HTML seja analisado antes de começar a construir e definir o layout da árvore de renderização. Partes do conteúdo serão analisadas e exibidas, enquanto o processo continua com o restante do conteúdo que continua vindo 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

Nas figuras 3 e 4, é possível ver que, embora o WebKit e o Gecko usem terminologias um pouco diferentes, o fluxo é basicamente o mesmo.

O 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 de elementos, enquanto a Gecko 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 do 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 muito significativo dentro do mecanismo de renderização, vamos nos aprofundar um pouco mais nele. Vamos começar com uma breve introdução sobre a análise.

Analisar um documento significa convertê-lo em 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 obedece: a linguagem ou o formato em que foi escrito. Todo formato que pode ser analisado precisa ter gramática determinista composta de regras de vocabulário e sintaxe. Ele é chamado de gramática livre de contexto. As linguagens humanas não são essas linguagens e, portanto, não podem ser analisadas com técnicas de análise convencionais.

Combinação Analisador - Analisador léxico

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 do idioma em questão.

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

Os analisadores geralmente dividem o trabalho em dois componentes: o léxico (às vezes chamado de tokenizador), responsável por dividir a entrada em tokens válidos, e o analisador, responsável por construir a árvore de análise, analisando 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 às árvores de análise
Figura 6: do documento de origem às árvores de análise

O processo de análise é iterativo. O analisador normalmente solicita um novo token ao analisador léxico e tenta fazer a correspondência do token com uma das regras de sintaxe. Se uma regra for correspondida, 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 vai 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 na tradução: transformar o documento de entrada em outro formato. Um exemplo é a compilação. O compilador que compila o código-fonte em código de máquina primeiro o analisa em uma árvore de análise e, em seguida, converte 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 de análise a partir de uma expressão matemática. Vamos tentar definir uma linguagem matemática simples e ver o processo de análise.

Sintaxe:

  1. Os elementos básicos da sintaxe de uma 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" seguida por uma "operação" seguida por outro termo
  4. Uma operação é um token de adição ou subtração
  5. Um termo é um token de número inteiro ou uma expressão

Vamos analisar a entrada 2 + 3 - 1.

A primeira substring que corresponde a uma regra é 2: de acordo com a regra no 5, é um termo. A segunda correspondência é 2 + 3: 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 você pode ver, números inteiros são definidos por uma expressão regular.

A sintaxe geralmente é definida em um formato chamado BNF. Nossa linguagem é definida como:

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

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

Tipos de analisadores

Há dois tipos de analisadores: descendente e ascendente. Uma explicação intuitiva é que analisadores descendentes examinam a estrutura de alto nível da sintaxe e tentam encontrar uma correspondência com a regra. Analisadores ascendentes começam com a entrada e a transformam gradualmente em regras de sintaxe, começando pelas regras de nível inferior até que as regras de nível superior sejam atendidas.

Vamos conferir como os dois tipos de analisadores analisam nosso exemplo.

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

O analisador ascendente vai verificar a entrada até encontrar uma regra. A entrada correspondente será substituída pela regra. Isso vai continuar até o final da entrada. A expressão com correspondência parcial é colocada na pilha do analisador.

Empilhar Entrada
2 + 3 - 1
termo Mais de 3 a 1
operação do termo 3 a 1
expressão - 1
operação de expressão 1
expressão -

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

Geração automática de analisadores

Você fornece a eles a gramática da sua linguagem (seu vocabulário e regras de sintaxe) e eles geram um analisador em funcionamento. Criar um analisador exige uma compreensão profunda da análise e não é fácil criar um analisador otimizado manualmente, de modo que 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 criação de analisadores (talvez você os encontre pelos nomes Lex e Yacc). A entrada Flex é um arquivo que contém definições de expressões regulares 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 HTML são definidos em especificações criadas pela organização W3C.

Como vimos na introdução sobre análise, a sintaxe gramatical pode ser definida formalmente usando formatos como BNF.

Infelizmente, todos os tópicos convencionais sobre analisadores não se aplicam ao HTML (não os mencionei apenas 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.

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

Isso parece estranho à primeira vista. O HTML é bem parecido com o XML. Há muitos analisadores XML disponíveis. Existe uma variação XML do HTML, o XHT. Então, qual é a grande diferença?

A diferença é que a abordagem HTML é mais "perdoável": ela permite omitir certas tags (que são adicionadas implicitamente) ou, às vezes, omitir tags de início ou fim e assim por diante. No geral, é uma "soft" diferente da sintaxe rígida e exigente do XML.

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

DTD HTML

A definição de HTML está em um formato DTD. Esse formato é usado para definir linguagens da família SGML. O formato contém definições de 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 segue apenas as especificações, mas outros modos são compatíveis com marcações usadas pelos navegadores no passado. O objetivo é oferecer compatibilidade com conteúdos mais antigos. O DTD restrito atual está aqui: www.w3.org/TR/html4/strict.dtd

DOM

A árvore de saída (a "árvore de análise") é uma árvore de elementos DOM e nós de atributo. DOM é a sigla para Modelo de objeto de documentos. É a apresentação em objeto do documento HTML e da interface de elementos HTML para o mundo externo, como JavaScript.

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

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 do DOM da marcação de exemplo

Assim como o HTML, o DOM é especificado pela organização W3C. Consulte www.w3.org/DOM/DOMTR (em inglês). É uma especificação genérica para a manipulação de 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.

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

O algoritmo de análise

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

Os motivos são:

  1. A natureza pacífica da linguagem.
  2. O fato de os navegadores terem tolerância a erros tradicional para suportar 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. Assim, o processo de análise modifica a entrada.

Não é possível usar as técnicas comuns de análise, e os navegadores criam analisadores personalizados para análise de HTML.

O algoritmo de análise é descrito em detalhes pela 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 start, end e 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 fim da entrada.

Fluxo de análise HTML (tirado das especificações do HTML5)
Figura 9: fluxo de análise HTML (tirado 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 estado. 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 de tokenização atual e pelo estado de construção da árvore. Isso significa que o mesmo caractere consumido produz resultados diferentes para o estado correto seguinte, dependendo do estado atual. O algoritmo é complexo demais para ser descrito de forma completa. Por isso, vejamos um exemplo simples que nos ajudará a entender o princípio.

Exemplo básico - tokenizando o seguinte HTML:

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

O estado inicial é o "Estado de dados". Quando o caractere < é encontrado, o estado é alterado para "Estado de tag aberta". O consumo de um caractere a-z causa a criação de um "Token de tag de início" e 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 token html.

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

Agora estamos de volta ao Estado aberto da tag. Consumir a próxima entrada / causa a criação de uma end tag token e uma mudança para o "Estado do nome da tag". Novamente, permanecemos nesse estado até atingirmos >.Em seguida, o novo token de tag será emitido e voltaremos ao "Estado de dados". A entrada </html> será tratada como o caso anterior.

Tokenização da entrada de exemplo
Figura 10: tokenização da entrada de exemplo

Algoritmo de construção de á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 é modificada e elementos são adicionados a ele. Cada nó emitido pelo tokenizador é processado pelo construtor da árvore. Para cada token, a especificação define qual elemento DOM é relevante e será criado para esse 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 estado. 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". Receber o "html" causará uma transferência para o modo "pré-html" e um reprocessamento do token nesse modo. Isso causa a criação do elemento HTMLHTMLElement, que será anexado ao objeto Document raiz.

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

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

Os tokens de caractere do "Hello world" serão recebidas. A primeira causa a criação e inserção de um "Texto" nó e os outros caracteres serão anexados a ele.

O recebimento do token de fim do body causa a transferência para o modo "após o body". Agora receberemos a tag de término html, que nos levará para o modo "after after body". O recebimento do final do token do arquivo encerrará a análise.

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

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

Nesta fase, o navegador marcará o documento como interativo e começará a analisar os scripts que estão "adiados" mode: aqueles que devem ser executados após a análise do documento. O estado do documento será definido como "concluído". e uma "carga" será acionado.

Veja os algoritmos completos de tokenização e construção de árvore na especificação HTML5 (em inglês).

Navegadores tolerância a erros

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

Veja este HTML, por exemplo:

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

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

A manipulação de erros é bastante consistente em navegadores, mas surpreendentemente não faz parte das especificações HTML. Assim como os botões de adicionar aos favoritos e voltar/avançar, é algo que foi desenvolvido nos navegadores ao longo dos anos. Há construções de HTML inválidas que são conhecidas e repetidas em muitos sites, e os navegadores tentam corrigi-las de maneira compatível com outros navegadores.

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

O analisador analisa entradas tokenizadas no documento, construindo a árvore do documento. Se o documento estiver bem estruturado, a análise será simples.

Infelizmente, temos que lidar com muitos documentos HTML que não são bem formados, por isso o analisador deve ser tolerante em relação a erros.

Precisamos tratar, pelo menos, das seguintes condições de erro:

  1. O elemento que está sendo adicionado é expressamente proibido dentro de alguma tag externa. Neste caso, devemos fechar todas as tags até a que proíbe o elemento e adicioná-lo em seguida.
  2. Não é permitido adicionar o elemento diretamente. Talvez a pessoa que escreveu o documento tenha esquecido de alguma tag no meio (ou que a tag intermediária 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 in-line até o próximo elemento de bloco superior.
  4. Se isso não ajudar, feche os elementos até que tenhamos permissão para adicioná-lo ou ignore a tag.

Vejamos 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 IE e Firefox, o WebKit trata isso 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 stray table é uma tabela dentro de outra tabela, mas não dentro de uma célula de tabela.

Exemplo:

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

O WebKit alterará 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 atual do elemento: ele destacará a tabela interna da pilha da tabela externa. As tabelas agora serão irmãs.

Elementos de formulário aninhados

Caso o usuário insira um formulário dentro de outro, o segundo formulário será 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 só.

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

Novamente, o comentário fala por si.

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

Portanto, os autores da web devem tomar cuidado: a menos que queiram aparecer como um exemplo em um snippet de código com tolerância a erros do WebKit, escrevam HTML bem formado.

Análise CSS

Lembra-se dos conceitos de análise apresentados na introdução? Bem, ao contrário do HTML, o CSS é uma gramática livre de contexto e pode ser analisada usando os tipos de analisador descritos na introdução. Na verdade, a especificação CSS define a gramática léxica e de sintaxe do CSS.

Vejamos alguns exemplos:

A gramática léxica (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}*

&quot;ident&quot; é a abreviação de identificador, como um nome de classe. "nome" é 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 tem 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. Esta 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 (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. "declaração" e "seletor" será definido nas definições de 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ê deve se lembrar da introdução sobre o analisador, o Bison cria um analisador shift-reduce de baixo para cima. O Firefox usa um analisador descendente escrito manualmente. Em ambos os casos, cada arquivo CSS é analisado em um objeto de folha de estilo. Cada objeto contém regras de 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. Autores esperam que os scripts sejam analisados e executados imediatamente quando o analisador atinge uma tag <script>. A análise do documento é interrompida até que o script seja executado. Se o script for externo, primeiro o recurso deve ser buscado na rede. Isso também é feito de maneira síncrona, e a análise é interrompida até que o recurso seja buscado. Este foi o modelo por muitos anos e também está incluído nas especificações HTML4 e 5. Autores podem adicionar a restrição a um script, caso em que ele não vai interromper a análise do documento e será executado depois que o documento for analisado. 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, 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 analisa apenas referências a recursos externos, como scripts externos, folhas de estilo e imagens. Ele não modifica a árvore do DOM, que é deixada 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 de 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, aparentemente, isso causou muitos problemas. Parece ser um caso extremo, mas muito 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 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 os filhos dele.

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 geralmente correspondente a uma caixa de CSS do nó, conforme descrito nas especificações de CSS2. Ele inclui informações geométricas como largura, altura e posição.

O tipo de caixa é afetado pelo atributo valor do atributo de estilo relevante para o nó. Consulte a seção computação de estilo. Aqui está 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 frames especiais.

No WebKit, se um elemento quiser criar um renderizador especial, ele 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 do DOM

Os renderizadores correspondem aos elementos DOM, mas a relação não é de um para um. Elementos DOM não visuais não são inseridos na árvore de renderização. Um exemplo é o "head" . Também elementos cujo valor de exibição foi atribuído a "nenhum" não aparecerá na árvore (enquanto os elementos com visibilidade "oculta" 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 valor "select" tem três renderizadores: um para a área de exibição, um para a caixa de lista 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 uso 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 apenas elementos inline. No caso de conteúdo misto, renderizadores de bloco 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. Flutuações 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 do DOM correspondente. A "Janela de visualização" é o bloco contendo inicial. No WebKit, ele será o "RenderView". objeto

O fluxo de construção da árvore

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

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

O processamento das tags html e body resulta na construção da raiz da árvore de renderização. O objeto de renderização raiz corresponde ao que a especificação CSS chama de bloco que 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 a chama de RenderView. Esse é o objeto de renderização para o qual o documento aponta. O resto da árvore é construído como uma inserção de nós DOM.

Consulte a especificação 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 calculando as propriedades de estilo de cada elemento.

O estilo inclui folhas de estilo de várias origens, elementos de estilo inline e propriedades visuais no HTML (como a propriedade "bgcolor").O mais recente é traduzido para propriedades de estilo CSS correspondentes.

As origens 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 no "Perfil do Firefox" ).

A computação de estilo apresenta algumas dificuldades:

  1. Os dados de estilo são uma construção muito grande que contém diversas propriedades de estilo e 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. Percorrer a lista de regras inteira para 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 inicie 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{
    ...
    }
    

    Significa que as regras se aplicam a uma <div> que é descendente de três divs. Suponha que você queira verificar se a regra se aplica a um determinado elemento <div>. Você escolhe um determinado caminho na árvore para verificar. Talvez seja necessário percorrer a árvore de nós para descobrir que existem apenas dois divs e que a regra não se aplica. Em seguida, você precisa tentar outros caminhos na árvore.

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

Vamos conferir 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 pelos nós em algumas condições. Os nós são irmãos ou primos e:

  1. Os elementos precisam estar no mesmo estado do mouse (por exemplo, um não pode estar em :hover enquanto o outro não está)
  2. Nenhum elemento pode ter um ID
  3. Os nomes das tags devem ser iguais
  4. Os atributos de classe devem corresponder
  5. O conjunto de atributos mapeados precisa ser idêntico
  6. Os estados do link devem corresponder
  7. Os estados de foco precisam ser correspondentes
  8. Nenhum elemento deve ser afetado por seletores de atributo, em que "afetado" é definido como uma correspondência de seletor que usa um seletor de atributo em qualquer posição dentro do seletor
  9. Não deve haver atributo de estilo in-line nos elementos
  10. Não pode haver seletores derivados em uso. O WebCore simplesmente aciona 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 seletores como :first-child e :last-child.

Árvore de regras do Firefox

O Firefox possui duas árvores extras para facilitar o cálculo de 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 seu estilo relevante.

Árvore de contexto de estilo do Firefox.
Figura 14: árvore de contexto de estilo do 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 para 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 é muito inteligente. Ele permite o compartilhamento desses valores entre 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 computados são adicionados à árvore.

A ideia é ver os caminhos da árvore como palavras em um léxico. Digamos que essa árvore de regras já tenha sido computada:

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

Suponha que precisamos fazer a correspondência de 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á computamos o caminho A-B-E-I-L. Agora, teremos menos trabalho a fazer.

Vamos ver como a árvore nos ajuda a trabalhar.

Divisão em estruturas

Os contextos de estilo são divididos em estruturas. Esses structs 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. 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.

A árvore nos ajuda a armazenar em cache estruturas inteiras (contendo os valores finais computados) na árvore. A ideia é que, se o nó inferior não fornecesse uma definição para uma estrutura, uma estrutura armazenada em cache em um nó superior pode 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 existente. Em seguida, começamos a aplicar as regras no caminho para preencher as estruturas no novo contexto de estilo. Começamos no nó inferior do caminho, aquele com a precedência mais alta (normalmente, o seletor mais específico), e atravessamos a árvore até que nosso struct esteja completo. Se não houver especificação para a estrutura nesse nó de regra, podemos otimizar bastante. Subimos na árvore até encontrarmos um nó que a especifique totalmente e apontamos para ela. Essa é a melhor otimização: toda a estrutura é compartilhada. Isso economiza computação de valores finais e memória.

Quando encontramos definições parciais, subimos na árvore até que o struct seja preenchido.

Se não encontramos nenhuma definição para o struct, caso ele seja "herdado" apontamos para o struct do pai na árvore de contexto. Nesse caso, também compartilhamos structs com sucesso. 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á-los em valores reais. Em seguida, 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ó da árvore, o contexto de estilo completo pode ser compartilhado entre eles.

Vejamos um exemplo: Suponha que temos este 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, digamos que precisamos preencher apenas duas estruturas: a estrutura de cor e a estrutura de margem. O struct de cor contém apenas um membro: a cor O struct margin contém os quatro lados.

A árvore de regras resultante terá esta aparência (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 o qual elas 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. Começaremos preenchendo a estrutura de margem. Como o último nó de regra (F) não é adicionado à estrutura de margem, podemos subir na árvore até encontrar e usar uma estrutura em cache calculada em uma inserção de nó anterior. Nós a encontraremos no nó B, que é o nó superior que especificou regras de margem.

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

O trabalho no segundo elemento <span> é ainda mais fácil. Corresponderemos as regras e chegaremos à conclusão 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 de estilo e apenas apontar para o contexto do período anterior.

Para estruturas com 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 adicionarmos 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 mesma estrutura de fonte que o pai. Isso ocorrerá se nenhuma regra de fonte for especificada para o parágrafo.

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

Para resumir: compartilhar os objetos de estilo (inteiramente ou algumas das estruturas 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

Há 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 tipos são facilmente combinados com o elemento, pois ele possui os atributos de estilo e os atributos HTML podem ser mapeados usando o elemento como a chave.

Como observado anteriormente 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.

Depois de 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, por nome de classe, por 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 é necessário examinar cada declaração: podemos extrair as regras relevantes para um elemento dos mapas. Essa otimização elimina mais de 95% das regras, para que elas não precisem ser consideradas durante o processo de correspondência(4.1).

Vamos conferir, 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 classe. A segunda no mapa de IDs e a terceira no mapa de tags.

Para o fragmento HTML a seguir:

<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 da classe conterá um "erro" chave sob a qual a regra para "p.error" for encontrado. O elemento div terá regras relevantes no mapa de id (a chave é o id) e no mapa de tag. Assim, o único trabalho que resta é descobrir qual das regras extraídas pelas chaves realmente correspondem.

Por exemplo, se a regra para o div fosse:

table div {margin: 5px}

Ela ainda será extraída 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 possui propriedades que correspondem a cada atributo visual (todos os atributos CSS, mas as mais genéricas). 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. Aqui vem a ordem em cascata para resolver o problema.

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

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

As declarações do navegador são menos importantes e o usuário substitui o autor somente se a declaração tiver sido marcada como importante. Declarações com a mesma ordem são classificadas por especificidade e, em seguida, 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 (link em inglês) da seguinte maneira:

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

Concatenar os quatro números a-b-c-d (em um sistema de números com uma base grande) resulta na especificidade.

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

Por exemplo, se a=14, você pode usar a base hexadecimal. No caso improvável de a=17, você precisará de uma base numérica de 17 dígitos. A situação posterior pode acontecer com um seletor como este: html body div div p... (17 tags em 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 */

Ordenar 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 listas 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 de posição serão usados, marcados no documento e serão recalculados assim que as folhas de estilo forem carregadas.

Layout

Quando o renderizador é criado e adicionado à árvore, ele não tem uma posição e um 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 transmissão. Elementos mais tarde "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 pelo documento. Há exceções: por exemplo, tabelas HTML podem exigir mais de uma passagem.

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 a hierarquia de frames ou parte dela, calculando informações geométricas para cada renderizador que a exige.

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 "layout" ou "reflow" , cada renderizador invoca o método de layout dos filhos que precisam de layout.

Sistema de bits sujos

Para não gerar um layout completo para cada pequena alteração, os navegadores usam um "bit sujo" sistema. Um renderizador alterado ou adicionado marca a si mesmo e aos filhos como "incorretos": precisando de layout.

Há duas flags: "dirty" e "children are dirty" o que significa que, embora o renderizador esteja correto, 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, que é "global" o mesmo layout organizacional. Isso pode acontecer como resultado de:

  1. Uma mudança de estilo global que afeta todos os renderizadores, como uma mudança no tamanho da fonte.
  2. Como resultado de uma tela ter sido redimensionada

O layout pode ser incremental, apenas os renderizadores sujos vão aparecer (isso pode causar danos que vão exigir layouts extras).

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 o conteúdo extra veio da rede e foi adicionado à árvore DOM.

Layout incremental.
Figura 18: layout incremental - apenas renderizadores incorretos e seus filhos estão dispostos

Layout assíncrono e síncrono

O layout incremental é feito de forma assíncrona. O Firefox coloca os "comandos de reflow" em fila para layouts incrementais, e um programador aciona a execução em lote desses comandos. O WebKit também possui um temporizador que executa um layout incremental: a árvore é atravessada e fica "suja" os renderizadores são gerados fora de layout.

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

O layout global normalmente é 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, mudaram.

Otimizações

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

Em alguns casos, apenas uma subárvore é modificada e o layout não é iniciado da raiz. Isso pode acontecer nos casos em que a mudança é local e não afeta o entorno, como texto inserido em campos de texto (caso contrário, cada tecla pressionada acionaria um layout a partir da raiz).

O processo de layout

O layout geralmente tem o seguinte padrão:

  1. O renderizador pai determina a própria largura.
  2. O pai vai até as crianças e:
    1. Posiciona o renderizador filho (define seu x e y).
    2. Chama o layout filho se necessário. Ele está incorreto, estamos em um layout global ou, por algum outro motivo, calcula a altura do filho.
  3. O elemento pai usa as alturas cumulativas dos filhos e as alturas das margens e do preenchimento para definir a própria altura, que será usada pelo pai do renderizador pai.
  4. Define seu bit incorreto como falso.

O Firefox usa um "estado" objeto(nsHTMLReflowState) como um parâmetro para o layout (denominado "reflow"). Entre outros, o estado inclui a largura do elemento pai.

A saída do layout do Firefox é uma "métrica" objeto(nsHTMLReflowMetrics). Ele contém a altura computada do renderizador.

Cálculo da largura

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

Por exemplo, a largura deste div:

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

Seria calculado pelo WebKit como o seguinte(classe RenderBox método calcWidth):

  • A largura do contêiner é a máxima dos contêineres availableWidth e 0. A availableWidth neste caso é contentWidth, calculada como:
clientWidth() - paddingLeft() - paddingRight()

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

  • A largura dos elementos é a "largura" . Ela será calculada como um valor absoluto calculando a porcentagem da largura do contêiner.

  • As bordas horizontais e preenchimentos foram adicionados.

Até o momento, este 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 é necessário quebrar, ele para e propaga ao pai do layout que ele precisa ser corrompido. O pai cria os renderizadores extras e chama o layout neles.

Pintura

Na fase de pintura, a árvore de renderização é atravessada e o método "paint()" do renderizador é chamado para exibir conteúdo na tela. A pintura usa o componente de infraestrutura da interface.

Global e incremental

Assim como o layout, a pintura também pode ser global (a árvore inteira é pintada) ou incremental. Na pintura incremental, alguns dos renderizadores são alterados de uma 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 gerar uma "pintura" evento. O SO faz isso de maneira inteligente e une várias regiões em uma só. 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 ouve 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 a seus 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, já que as pilhas são pintadas de trás para a frente. A ordem de empilhamento de um renderizador em bloco é:

  1. cor do plano de fundo
  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. Ela contém os renderizadores relevantes para o retângulo, na ordem de pintura correta (planos de fundo dos renderizadores, depois bordas etc.).

Dessa forma, a árvore precisa ser atravessada apenas uma vez para uma repintura em vez de 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 serão escondidos, como elementos completamente sob outros elementos opacos.

Armazenamento em retângulo do WebKit

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

Mudanças dinâmicas

Os navegadores tentam fazer o mínimo de ações possível em resposta a uma mudança. Portanto, as mudanças na cor de um elemento causarão apenas a reformulação dele. Mudanças na posição do elemento vão causar uma nova pintura do layout e da nova pintura dele, dos filhos e, possivelmente, dos irmãos. Adicionar um nó DOM causará o layout e a nova pintura do nó. Mudanças importantes, como o aumento do tamanho da fonte do "html" causará a invalidação de caches, novo layout ou 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 uma única linha de execução. No Firefox e no Safari, essa é a linha de execução principal 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 duas a seis 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

O canvas

De acordo com a especificação CSS2 (link 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 (em inglês), o canvas é transparente se contido em outro e, caso contrário, recebe 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 preenchimento opcional ao redor, borda e áreas de margem.

Modelo de box CSS2
Figura 19: modelo de box CSS2

Cada nó gera 0...n essas caixas.

Todos os elementos têm uma "exibição" 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 estilo do navegador pode definir outros padrões. Por exemplo: a exibição padrão para o "div" é um bloco.

Veja um exemplo de folha de estilo padrão aqui: www.w3.org/TR/CSS2/sample.html (em inglês).

Esquema de posicionamento

Existem 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 seu lugar na árvore do DOM e disposto de acordo com o tipo e as dimensões da caixa.
  2. Flutuante: o objeto é apresentado primeiro como um fluxo normal e, em seguida, é 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 do DOM

O esquema de posicionamento é definido pela "posição" e a propriedade "float" .

  • estático e relativo geram um fluxo normal
  • absoluta e fixa causa posicionamento absoluto

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

O layout do box é determinado pelos seguintes fatores:

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

Tipos de box

Caixa de 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 o próprio bloco, mas está dentro de um bloco que contém.

Caixas inline.
Figura 21: box inline

Os blocos são formatados verticalmente um após o outro. Os inline são formatados horizontalmente.

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

Boxes inline são colocados dentro de linhas ou "boxes em linha". As linhas são pelo menos tão altas quanto a caixa mais alta, mas podem ser mais altas quando as caixas estão alinhadas na "linha de base" - ou seja, a parte inferior de um elemento está alinhada em um ponto de outra caixa, diferente da parte inferior. Se a largura do contêiner não for suficiente, as in-lines serão colocadas em várias linhas. Geralmente, é isso que acontece em um parágrafo.

Linhas
Figura 23: linhas

Posicionamento

Relativo

Posicionamento relativo - posicionado como de costume e movido para o delta obrigatório.

Posicionamento relativo.
Figura 24: posicionamento relativo

Variações

Um box flutuante é deslocado para a esquerda ou para a direita de uma linha. O interessante é que as outras caixas fluem ao redor 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. No formato fixo, o contêiner é a janela de visualização.

Posicionamento fixo.
Figura 26: posicionamento fixo

Representação em camadas

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

Os boxes são divididos em pilhas (chamadas de empilhamento de contextos). Em cada pilha, os elementos de fundo serão pintados primeiro, e os elementos de frente para cima, mais perto do usuário. Em caso de sobreposição, o elemento mais alto ocultará o anterior.

As pilhas são ordenadas de acordo com a propriedade do Z-index. Caixas com "z-index" de 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 a div vermelha preceda a verde na marcação e tenha sido pintada antes no fluxo normal, a propriedade z-index é maior, ou seja, está mais à frente na pilha mantida pelo box 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 o "Dragon book"), Addison-Wesley, 1986
    2. o Rick Jelliffe. Ousado e bonito: 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) (em inglês)
    3. L. David Baron, Layout Engine do Mozilla
    4. L. David Baron, Documentação do Mozilla Style System
    5. Chris Waterson, Notes on HTML Reflow (link 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 o CSS(parte 1)
    2. David Hyatt, An Overview of WebCore
    3. David Hyatt, WebCore Rendering
    4. David Hyatt, The FOUC Problem
  5. Especificações W3C

    1. Especificação HTML 4.01
    2. Especificação HTML5 do W3C
    3. Especificação do Cascading Style Sheets nível 2 Revisão 1 (CSS 2.1)
  6. Instruções de criação de navegadores

    1. Firefox https://developer.mozilla.org/Build_Documentation
    2. WebKit. http://webkit.org/building/build.html
.

Traduções

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

Você pode conferir as traduções hospedadas externamente de Coreano e Turco.

Agradecemos a todos!