Cómo funcionan los navegadores

El detrás de escena de los navegadores web modernos

Prefacio

Este manual integral sobre las operaciones internas de WebKit y Gecko es el resultado de muchas investigaciones realizadas por el desarrollador israelí Tali Garsiel. En el transcurso de unos años, revisó todos los datos publicados sobre los componentes internos de los navegadores y dedicó mucho tiempo a leer el código fuente del navegador web. Ella escribió:

Como desarrollador web, aprender los aspectos internos de las operaciones del navegador te ayuda a tomar mejores decisiones y a conocer las justificaciones detrás de las prácticas recomendadas de desarrollo. Si bien este documento es bastante extenso, te recomendamos que dediques tiempo a investigarlo. Te alegrarás de haberlo hecho.

Paul Ireland, Relaciones con Desarrolladores de Chrome

Introducción

Los navegadores web son el software más usado. En este manual, explico cómo funcionan detrás de escena. Veremos lo que sucede cuando escribes google.com en la barra de direcciones hasta que veas la página de Google en la pantalla del navegador.

Navegadores sobre los que hablaremos

En la actualidad, se usan los cinco principales navegadores para computadoras de escritorio: Chrome, Internet Explorer, Firefox, Safari y Opera. En los dispositivos móviles, los navegadores principales son el navegador de Android, iPhone, Opera Mini y Opera Mobile, el navegador UC, los navegadores Nokia S40/S60 y Chrome. Todos ellos, a excepción de los navegadores Opera, se basan en WebKit. Proporcionaré ejemplos de los navegadores de código abierto Firefox y Chrome, y Safari (que es en parte de código abierto). Según las estadísticas de StatCounter (hasta junio de 2013), Chrome, Firefox y Safari representan alrededor del 71% del uso global de los navegadores de escritorio. En los dispositivos móviles, el navegador Android, iPhone y Chrome constituyen alrededor del 54% del uso.

La funcionalidad principal del navegador

La función principal de un navegador es presentar el recurso web que elijas solicitándolo al servidor y mostrándolo en la ventana del navegador. El recurso suele ser un documento HTML, pero también puede ser un PDF, una imagen o algún otro tipo de contenido. El usuario especifica la ubicación del recurso mediante un URI (identificador uniforme de recursos).

La forma en que el navegador interpreta y muestra los archivos HTML se especifica en las especificaciones HTML y CSS. La organización W3C (World Wide Web Consortium), que es la organización de estándares para la Web, mantiene estas especificaciones. Durante años, los navegadores se adaptaron solo a una parte de las especificaciones y desarrollaron sus propias extensiones. Eso causó graves problemas de compatibilidad para los autores de la Web. Hoy en día, la mayoría de los navegadores cumplen más o menos con las especificaciones.

Las interfaces de usuario de los navegadores tienen mucho en común. Entre los elementos comunes de la interfaz de usuario se encuentran los siguientes:

  1. Barra de direcciones para insertar un URI
  2. Botones Atrás y Avanzar
  3. Opciones de favoritos
  4. Botones de actualización y detención para actualizar o detener la carga de los documentos actuales
  5. Botón de la página principal que te lleva a la página principal

Curiosamente, la interfaz de usuario del navegador no se especifica en ninguna especificación formal, sino que proviene de las buenas prácticas definidas a lo largo de los años de experiencia y de navegadores que se imitan entre sí. La especificación HTML5 no define los elementos de la IU que debe tener un navegador, sino que enumera algunos comunes. Entre ellas, se incluyen la barra de direcciones, la barra de estado y la barra de herramientas. Por supuesto, existen funciones exclusivas de un navegador específico, como el administrador de descargas de Firefox.

Infraestructura de alto nivel

Los componentes principales del navegador son los siguientes:

  1. Interfaz de usuario: Incluye la barra de direcciones, el botón Atrás/adelante, el menú de favoritos, etc. Se muestran todas las partes del navegador, excepto la ventana en la que se muestra la página solicitada.
  2. Motor del navegador: Reúne las acciones entre la IU y el motor de renderización.
  3. Motor de renderización: Es responsable de mostrar el contenido solicitado. Por ejemplo, si el contenido solicitado es HTML, el motor de renderización analiza HTML y CSS, y muestra el contenido analizado en la pantalla.
  4. Herramientas de redes: Para llamadas de red, como solicitudes HTTP, con diferentes implementaciones para distintas plataformas detrás de una interfaz independiente de la plataforma.
  5. Backend de IU: Se usa para dibujar widgets básicos, como cuadros combinados y ventanas. Este backend expone una interfaz genérica que no es específica de una plataforma. Debajo, usa métodos de interfaz de usuario del sistema operativo.
  6. Intérprete de JavaScript. Se usa para analizar y ejecutar código JavaScript.
  7. Almacenamiento de datos. Esta es una capa de persistencia. Es posible que el navegador deba guardar todo tipo de datos a nivel local, como cookies. Los navegadores también admiten mecanismos de almacenamiento como localStorage, IndexedDB, WebSQL y FileSystem.
Componentes del navegador
Figura 1: Componentes del navegador

Es importante tener en cuenta que los navegadores como Chrome ejecutan varias instancias del motor de renderización: una por cada pestaña. Cada pestaña se ejecuta en un proceso independiente.

Motores de renderización

La responsabilidad del motor de renderización es adecuada. Representar datos, es decir, mostrar el contenido solicitado en la pantalla del navegador.

De forma predeterminada, el motor de renderización puede mostrar imágenes y documentos HTML y XML. Puede mostrar otros tipos de datos mediante complementos o extensiones; por ejemplo, mostrar documentos PDF con un complemento de visualización de PDF. Sin embargo, en este capítulo nos centraremos en el caso de uso principal: mostrar HTML e imágenes con formato CSS.

Los distintos navegadores usan distintos motores de representación: Internet Explorer usa Trident, Firefox usa Gecko, Safari usa WebKit. Chrome y Opera (a partir de la versión 15) usan Blink, una rama de WebKit.

WebKit es un motor de renderización de código abierto que comenzó como un motor para la plataforma Linux y Apple lo modificó para admitir Mac y Windows.

El flujo principal

El motor de renderización comenzará a obtener el contenido del documento solicitado desde la capa de red. Por lo general, esto se hace en fragmentos de 8 KB.

A continuación, este es el flujo básico del motor de renderización:

Flujo básico del motor de renderización
Figura 2: Flujo básico del motor de renderización

El motor de renderización comenzará a analizar el documento HTML y convertirá los elementos en nodos del DOM en un árbol llamado "árbol de contenido". El motor analizará los datos de estilo, tanto en los archivos CSS externos como en los elementos de estilo. Se usará la información de diseño junto con las instrucciones visuales en el código HTML para crear otro árbol: el árbol de renderización.

El árbol de renderización contiene rectángulos con atributos visuales, como el color y las dimensiones. Los rectángulos están en el orden correcto para aparecer en la pantalla.

Después de la construcción del árbol de renderización, pasa por un proceso de “diseño”. Esto significa darle a cada nodo las coordenadas exactas donde debería aparecer en la pantalla. La siguiente etapa es la de pintura: se recorrerá el árbol de renderización y se pintará cada nodo con la capa de backend de la IU.

Es importante entender que este es un proceso gradual. Para mejorar la experiencia del usuario, el motor de renderización intentará mostrar el contenido en la pantalla lo antes posible. No esperará a que se analice todo el HTML para comenzar a compilar y diseñar el árbol de renderización. Algunas partes del contenido se analizarán y se mostrarán, mientras que el proceso continúa con el resto del contenido que sigue procedente de la red.

Ejemplos de flujo principal

Flujo principal de WebKit
Figura 3: Flujo principal de WebKit
Flujo principal del motor de renderización Gecko de Mozilla.
Figura 4: Flujo principal del motor de renderización de Gecko de Mozilla

En las figuras 3 y 4, se puede ver que, aunque WebKit y Gecko usan terminología ligeramente diferente, el flujo es básicamente el mismo.

Gecko denomina "árbol de marcos" al árbol de elementos con formato visual. Cada elemento es un marco. WebKit usa el término “Árbol de representación” y consta de “Objetos de renderización”. WebKit usa el término "diseño" para la colocación de elementos, mientras que Gecko lo llama "reprocesamiento". "Attachment" es el término de WebKit para conectar nodos del DOM e información visual con el objetivo de crear el árbol de representación. Una diferencia no semántica menor es que Gecko tiene una capa adicional entre el HTML y el árbol del DOM. Se denomina "receptor de contenido" y es una fábrica para crear elementos del DOM. Hablaremos sobre cada parte del flujo:

Análisis - general

Debido a que el análisis es un proceso muy importante dentro del motor de renderización, lo profundizaremos un poco más. Comencemos con una pequeña introducción sobre el análisis.

Analizar un documento significa traducirlo a una estructura que el código pueda usar. El resultado del análisis suele ser un árbol de nodos que representan la estructura del documento. Esto se denomina árbol de análisis o árbol de sintaxis.

Por ejemplo, si analizas la expresión 2 + 3 - 1, se podría mostrar este árbol:

Nodo del árbol de expresiones matemáticas.
Figura 5: Nodo de árbol de expresiones matemáticas

Gramática

El análisis se basa en las reglas de sintaxis que el documento obedece: el lenguaje o formato en el que se escribió. Cada formato que puedas analizar debe tener una gramática determinista compuesta por vocabulario y reglas de sintaxis. Se denomina gramática libre de contexto. Los lenguajes humanos son otros, por lo que no se pueden analizar con las técnicas de análisis convencionales.

Analizador - Combinación Lexer

El análisis puede separarse en dos subprocesos: análisis léxico y análisis sintáctico.

El análisis léxico es el proceso de dividir la entrada en tokens. Los tokens son el vocabulario del lenguaje: la colección de componentes básicos válidos. En lenguaje humano, constará de todas las palabras que aparecen en el diccionario de ese idioma.

El análisis sintáctico consiste en aplicar las reglas sintácticas del lenguaje.

Los analizadores suelen dividir el trabajo entre dos componentes: el lexer (a veces llamado tokenizador) que se encarga de dividir la entrada en tokens válidos y el analizador que se encarga de construir el árbol de análisis mediante el análisis de la estructura del documento de acuerdo con las reglas de sintaxis del lenguaje.

El Lexer sabe cómo eliminar los caracteres irrelevantes, como los espacios en blanco y los saltos de línea.

Del documento fuente a los árboles de análisis
Figura 6: Del documento de origen a los árboles de análisis

El proceso de análisis es iterativo. Por lo general, el analizador solicitará un token nuevo al lexer y, luego, intentará hacer coincidir el token con una de las reglas de sintaxis. Si una regla coincide, se agregará al árbol de análisis el nodo correspondiente al token, y el analizador solicitará otro token.

Si no coincide ninguna regla, el analizador almacenará el token de forma interna y seguirá pidiendo tokens hasta que encuentre una que coincida con todos los tokens almacenados internamente. Si no se encuentra ninguna regla, el analizador generará una excepción. Esto significa que el documento no era válido y contenía errores de sintaxis.

Traducción

En muchos casos, el árbol de análisis no es el producto final. El análisis suele usarse en la traducción, es decir, la transformación del documento de entrada a otro formato. Un ejemplo es la compilación. El compilador que compila el código fuente en código máquina primero lo analiza en un árbol de análisis y, luego, lo traduce en un documento de código máquina.

Flujo de compilación
Figura 7: Flujo de compilación

Ejemplo de análisis

En la figura 5, creamos un árbol de análisis a partir de una expresión matemática. Intentemos definir un lenguaje matemático simple y veamos el proceso de análisis.

Sintaxis:

  1. Los componentes básicos de la sintaxis del lenguaje son expresiones, términos y operaciones.
  2. Nuestro lenguaje puede incluir cualquier cantidad de expresiones.
  3. Una expresión se define como un “término” seguido de una “operación” seguida de otro término
  4. Una operación es un token de suma o token de signo menos.
  5. Un término es un token de número entero o una expresión

Analicemos el 2 + 3 - 1 de entrada.

La primera substring que coincide con una regla es 2: según la regla 5, es un término. La segunda coincidencia es 2 + 3: coincide con la tercera regla: un término seguido de una operación seguido de otro término. La próxima coincidencia solo se alcanzará al final de la entrada. 2 + 3 - 1 es una expresión porque ya sabemos que 2 + 3 es un término, así que tenemos un término seguido de una operación seguido de otro término. 2 + + no coincidirá con ninguna regla, por lo que no es una entrada válida.

Definiciones formales de vocabulario y sintaxis

Por lo general, el vocabulario se expresa con expresiones regulares.

Por ejemplo, nuestro lenguaje se definirá de la siguiente manera:

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

Como puedes ver, los números enteros se definen mediante una expresión regular.

Por lo general, la sintaxis se define en un formato llamado BNF. Nuestro lenguaje se definirá de la siguiente manera:

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

Dijimos que los analizadores regulares pueden analizar un idioma si su gramática es una gramática libre de contexto. Una definición intuitiva de una gramática sin contexto es una gramática que se puede expresar por completo en BNF. Para ver una definición formal, consulta el artículo de Wikipedia sobre gramática sin contexto.

Tipos de analizadores

Existen dos tipos de analizadores: los de arriba abajo y los de abajo hacia arriba. Una explicación intuitiva es que los analizadores de Top Down examinan la estructura de alto nivel de la sintaxis e intentan encontrar una coincidencia de regla. Los analizadores ascendentes comienzan con la entrada y la transforman gradualmente en reglas de sintaxis, comenzando por las reglas de bajo nivel hasta que se cumplen las de alto nivel.

Veamos cómo los dos tipos de analizadores analizarán nuestro ejemplo.

El analizador de Top Down comenzará desde la regla de nivel superior: identificará 2 + 3 como una expresión. Luego, identificará 2 + 3 - 1 como una expresión (el proceso de identificación evoluciona y coincide con las otras reglas, pero el punto de inicio es la regla de nivel más alto).

El analizador de Bottom Up analizará la entrada hasta que coincida una regla. Luego, reemplazará la entrada coincidente con la regla. Continuará hasta el final de la entrada. La expresión con coincidencia parcial se coloca en la pila del analizador.

Pila Entrada
2 + 3 - 1
term + 3 - 1
operación de término 3 - 1
expresión : 1
operación de expresión 1
expresión -

Este tipo de analizador ascendente se denomina analizador de desplazamiento y reducción, ya que la entrada se desplaza hacia la derecha (imagina un puntero que apunta primero al inicio de la entrada y se mueve hacia la derecha) y se reduce gradualmente a reglas de sintaxis.

Genera analizadores automáticamente

Existen herramientas que pueden generar un analizador. Le proporcionas la gramática de tu idioma (su vocabulario y reglas de sintaxis) y generan un analizador que funcione. La creación de un analizador requiere un conocimiento profundo del análisis y no es fácil crear un analizador optimizado de forma manual, por lo que los generadores de analizadores pueden ser muy útiles.

WebKit usa dos generadores de analizadores conocidos: Flex para crear un lexer y Bison para crear un analizador (es posible que te encuentres con ellos con los nombres Lex y Yacc). La entrada de Flex es un archivo que contiene definiciones de expresiones regulares de los tokens. La entrada de Bison son las reglas de sintaxis del lenguaje en formato BNF.

Analizador HTML

El trabajo del analizador de HTML es analizar el lenguaje de marcado HTML en un árbol de análisis.

Gramática HTML

El vocabulario y la sintaxis de HTML se definen en las especificaciones creadas por la organización W3C.

Como vimos en la introducción al análisis, la sintaxis gramatical se puede definir formalmente con formatos como BNF.

Desafortunadamente, todos los temas de los analizadores convencionales no se aplican a HTML (no los mencioné por diversión, ya que se usarán para analizar CSS y JavaScript). El HTML no se puede definir fácilmente mediante una gramática libre de contexto que los analizadores necesitan.

Existe un formato formal para definir HTML, DTD (definición del tipo de documento), pero no es una gramática libre de contexto.

Esto parece extraño a primera vista; HTML es bastante parecido a XML. Hay muchos analizadores XML disponibles. Existe una variación XML de HTML (XHTML). Entonces, ¿cuál es la mayor diferencia?

La diferencia es que el enfoque HTML es más "perdón": permite omitir ciertas etiquetas (que luego se agregan implícitamente) u, a veces, omitir etiquetas de inicio o finalización, y así sucesivamente. En general, es una sintaxis “suave”, a diferencia de la sintaxis rígida y exigente de XML.

Este detalle aparentemente pequeño marca una gran diferencia. Por un lado, esta es la principal razón por la que el HTML es tan popular: perdona tus errores y le facilita la vida al autor de la Web. Por otro lado, dificulta la escritura de una gramática formal. Para resumir, los analizadores convencionales no pueden analizar el código HTML fácilmente, ya que su gramática no es independiente del contexto. Los analizadores de XML no pueden analizar HTML.

DTD de HTML

La definición HTML está en formato DTD. Este formato se usa para definir los idiomas de la familia SGML. El formato contiene definiciones para todos los elementos permitidos, sus atributos y jerarquía. Como vimos anteriormente, la DTD de HTML no forma una gramática libre de contexto.

Hay algunas variaciones de la DTD. El modo estricto cumple únicamente con las especificaciones, pero otros modos incluyen compatibilidad con el lenguaje de marcado usado por los navegadores en el pasado. Su propósito es brindar retrocompatibilidad con contenido más antiguo. El DTD estricto actual se encuentra aquí: www.w3.org/TR/html4/strict.dtd

DOM

El árbol de resultados (el "árbol analizado") es un árbol de nodos de atributos y elementos del DOM. DOM es la sigla en inglés de Document Object Model (Modelo de objetos del documento). Es la presentación de objetos del documento HTML y la interfaz de los elementos HTML al mundo exterior, como JavaScript.

La raíz del árbol es el objeto “Document”.

El DOM tiene una relación casi uno a uno con el lenguaje de marcado. Por ejemplo:

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

Este lenguaje de marcado se traduciría al siguiente árbol del DOM:

Árbol del DOM del lenguaje de marcado de ejemplo
Figura 8: Árbol del DOM del lenguaje de marcado de ejemplo

Al igual que HTML, la organización W3C especifica los DOM. Consulta www.w3.org/DOM/DOMTR. Es una especificación genérica para manipular documentos. Un módulo específico describe elementos específicos de HTML. Puedes encontrar las definiciones de HTML aquí: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.

Cuando digo que el árbol contiene nodos del DOM, me refiero al árbol con elementos que implementan una de las interfaces del DOM. Los navegadores usan implementaciones concretas que tienen otros atributos que el navegador usa internamente.

El algoritmo de análisis

Como vimos en las secciones anteriores, el HTML no se puede analizar con los analizadores normales de Top Down o Bottom Up.

Estos son los motivos:

  1. La naturaleza tolerante del lenguaje.
  2. El hecho de que los navegadores tengan tolerancia a errores tradicional para admitir casos conocidos de HTML no válido
  3. El proceso de análisis es reentrante. En el caso de otros lenguajes, la fuente no cambia durante el análisis, pero en HTML, el código dinámico (como elementos de secuencias de comandos que contienen llamadas a document.write()) puede agregar tokens adicionales, por lo que el proceso de análisis realmente modifica la entrada.

No se pueden usar las técnicas de análisis normales, por lo que los navegadores crean analizadores personalizados para analizar HTML.

El algoritmo de análisis se describe en detalle en la especificación HTML5. El algoritmo consta de dos etapas: la asignación de token y la construcción del árbol.

La asignación de token es el análisis léxico, en el que se analiza la entrada en tokens. Entre los tokens HTML están las etiquetas de inicio, las etiquetas de cierre, los nombres de atributos y los valores de atributos.

El tokenizador reconoce el token, se lo entrega al constructor del árbol, consume el carácter siguiente para reconocer el siguiente token y así sucesivamente hasta el final de la entrada.

Flujo de análisis de HTML (tomado de la especificación de HTML5)
Figura 9: Flujo de análisis de HTML (tomado de la especificación HTML5)

El algoritmo de asignación de token

El resultado del algoritmo es un token HTML. El algoritmo se expresa como una máquina de estados. Cada estado consume uno o más caracteres del flujo de entrada y actualiza el siguiente estado según esos caracteres. La decisión está influenciada por el estado actual de la asignación de token y por el estado de construcción del árbol. Esto significa que el mismo carácter consumido producirá diferentes resultados para el siguiente estado correcto, según el estado actual. El algoritmo es demasiado complejo para describirlo en su totalidad, así que veamos un ejemplo simple que nos ayude a comprender el principio.

Ejemplo básico: asignación de token al siguiente HTML:

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

El estado inicial es el "Estado de los datos". Cuando se encuentra el carácter <, el estado cambia a "Tag open state". El consumo de un carácter a-z hace que se cree un "Token de etiqueta de inicio"; el estado cambia a "Estado de nombre de etiqueta". Permanecemos en este estado hasta que se consume el carácter >. Cada carácter se agrega al nombre del token nuevo. En nuestro caso, el token creado es uno de html.

Cuando se alcanza la etiqueta >, se emite el token actual y el estado vuelve al "Estado de los datos". La etiqueta <body> se tratará con los mismos pasos. Hasta ahora, se emitieron las etiquetas html y body. Ahora estamos de vuelta en el "Estado de los datos". Si consumes el carácter H de Hello world, se creará y emitirá un token de caracteres, y esto continuará hasta que se alcance el < de </body>. Emitiremos un token de caracteres para cada carácter de Hello world.

Ahora estamos de vuelta en el "Estado abierto de la etiqueta". Consumir la siguiente / de entrada provocará la creación de una end tag token y un movimiento al "estado del nombre de la etiqueta". Una vez más, permanecemos en este estado hasta llegar a >.Luego, se emitirá el token de etiqueta nuevo y regresaremos al "Estado de datos". La entrada </html> se tratará como el caso anterior.

Asigna tokens a la entrada de ejemplo
Figura 10: Asignación de token de la entrada de ejemplo

Algoritmo de construcción de árboles

Cuando se crea el analizador, se crea el objeto Document. Durante la etapa de construcción del árbol, se modificará el árbol del DOM con el Documento en la raíz y se agregarán elementos. El constructor del árbol procesará cada nodo que emita el tokenizador. Para cada token, la especificación define qué elemento del DOM es relevante para cada token y se creará para este token. El elemento se agrega al árbol del DOM, así como a la pila de elementos abiertos. Esta pila se usa para corregir las faltas de coincidencia de anidación y las etiquetas no cerradas. El algoritmo también se describe como una máquina de estados. Los estados se denominan "modos de inserción".

Veamos el proceso de construcción de árbol para la entrada de ejemplo:

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

La entrada a la etapa de construcción del árbol es una secuencia de tokens de la etapa de asignación de token. El primer modo es el "modo inicial". Si recibes el token "html", se moverá al modo "before html", y el token se volverá a procesar en ese modo. Esto provocará la creación del elemento HTMLHtmlElement, que se agregará al objeto Document raíz.

El estado cambiará a "before head". Entonces, se recibe el token de "body". Se creará un HTMLHeadElement de forma implícita, aunque no tengamos un token de "head" y se agregará al árbol.

Ahora, pasamos al modo "in head" y, luego, "after head". Se vuelve a procesar el token de cuerpo, se crea e inserta un HTMLBodyElement y se transfiere el modo a "in body".

Ahora se reciben los tokens de caracteres de la cadena “Hello World”. El primero provocará la creación y la inserción de un nodo "Text" y los otros caracteres se agregarán a ese nodo.

La recepción del token de finalización del cuerpo provocará una transferencia al modo "after body". Ahora recibiremos la etiqueta de cierre HTML que nos moverá al modo "after after body". Recibir el token de fin del archivo finalizará el análisis.

Construcción de árbol del código HTML de ejemplo.
Figura 11: Construcción de árbol del HTML de ejemplo

Acciones cuando finaliza el análisis

En esta etapa, el navegador marcará el documento como interactivo y comenzará a analizar las secuencias de comandos que están en modo “diferido”: aquellas que se deben ejecutar después de analizar el documento. El estado del documento se establecerá como "complete" y se activará un evento "load".

Puedes ver los algoritmos completos para la asignación de token y la construcción de árboles en la especificación HTML5.

Tolerancia a errores de los navegadores

Nunca recibes un error de "Sintaxis no válida" en una página HTML. Los navegadores corrigen el contenido no válido y continúan.

Tomemos este código HTML como ejemplo:

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

Debo haber incumplido alrededor de un millón de reglas ("mytag" no es una etiqueta estándar, anidamiento incorrecto de los elementos "p" y "div", y otros), pero el navegador aun así lo muestra correctamente y no se queja. Por lo tanto, una gran parte del código del analizador corrige los errores del autor de HTML.

El manejo de errores es bastante coherente en los navegadores, pero sorprendentemente no se incluyó en las especificaciones HTML. Como los marcadores y los botones Atrás/adelante, es algo que se ha desarrollado en los navegadores a lo largo de los años. Se conocen construcciones de HTML no válidas que se repiten en muchos sitios, y los navegadores intentan corregirlas de manera tal que cumplan con los requisitos de otros navegadores.

La especificación HTML5 sí define algunos de estos requisitos. (WebKit lo resume muy bien en el comentario al comienzo de la clase de analizador HTML).

El analizador analiza la entrada con tokens en el documento y crea el árbol de documentos. Si el documento está bien formado, analizarlo es sencillo.

Lamentablemente, tenemos que manejar muchos documentos HTML que no tienen el formato correcto, por lo que el analizador debe ser tolerante a errores.

Debemos ocuparnos de, al menos, las siguientes condiciones de error:

  1. El elemento que se agrega está explícitamente prohibido dentro de alguna etiqueta externa. En este caso, debemos cerrar todas las etiquetas hasta la que prohíbe el elemento y agregarla más adelante.
  2. No podemos agregar el elemento directamente. Es posible que la persona que escribe el documento olvidó alguna etiqueta en el medio (o que la etiqueta que está entre ellas sea opcional). Este podría ser el caso con las siguientes etiquetas: HTML HEAD BODY TBODY TR TD LI (¿olvidé alguna?).
  3. Queremos agregar un elemento de bloque dentro de un elemento intercalado. Cierra todos los elementos intercalados hasta el siguiente elemento del bloque más alto.
  4. Si esto no funciona, cierra los elementos hasta que podamos agregar el elemento, o bien ignora la etiqueta.

Veamos algunos ejemplos de tolerancia a errores de WebKit:

</br> en lugar de <br>

Algunos sitios usan </br> en lugar de <br>. Para que sea compatible con IE y Firefox, WebKit los trata como a <br>.

El código:

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

Ten en cuenta que el manejo de errores es interno: no se presentará al usuario.

Una tabla desviada

Una tabla desviada es una tabla dentro de otra tabla, pero no dentro de una celda de la tabla.

Por ejemplo:

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

WebKit cambiará la jerarquía a dos tablas del mismo nivel:

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

El código:

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

WebKit usa una pila para el contenido actual de los elementos: la tabla interna sacará de la pila de la tabla externa. Las tablas ahora serán del mismo nivel.

Elementos de formulario anidados

Si el usuario coloca un formulario dentro de otro, se ignora el segundo formulario.

El código:

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

Una jerarquía de etiquetas demasiado profunda

El comentario habla por sí mismo.

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;
}

Etiquetas de cierre del cuerpo o HTML mal ubicadas

Nuevamente, el comentario habla por sí mismo.

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

Por lo tanto, los autores de la Web tienen cuidado: a menos que quieran aparecer como ejemplo en un fragmento de código de tolerancia a errores de WebKit, escriban HTML con un formato válido.

Análisis de CSS

¿Recuerdas los conceptos de análisis de la introducción? A diferencia de HTML, CSS es una gramática libre de contexto y se puede analizar mediante los tipos de analizadores descritos en la introducción. De hecho, la especificación de CSS define la gramática y la gramática léxica y sintáctica del CSS.

Veamos algunos ejemplos:

La gramática léxica (vocabulario) se define mediante expresiones 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" es la forma abreviada de identificar, como el nombre de una clase. "name" es el ID de un elemento (al que se refiere "#").

La gramática de la sintaxis se describe en 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*] ')' ]
  ;

Explicación:

Un conjunto de reglas es la siguiente estructura:

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

div.error y a.error son selectores. La parte dentro de las llaves contiene las reglas que aplica este conjunto de reglas. Esta estructura se define formalmente en esta definición:

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

Esto significa que un conjunto de reglas es un selector o, de forma opcional, varios selectores separados por comas y espacios (S significa espacio en blanco). Un conjunto de reglas contiene llaves y, dentro de ellas, una declaración o, opcionalmente, una cantidad de declaraciones separadas por punto y coma. “declaración” y “selector” se definirán en las siguientes definiciones de BNF.

Analizador de CSS de WebKit

WebKit usa generadores de analizadores Flex y Bison para crear analizadores automáticamente a partir de los archivos gramaticales de CSS. Como recuerdas de la introducción al analizador, Bison crea un analizador de Mayúsculas y reducción de abajo hacia arriba. Firefox usa un analizador de Top Down escrito manualmente. En ambos casos, cada archivo CSS se analiza en un objeto StyleSheet. Cada objeto contiene reglas CSS. Los objetos de regla CSS contienen objetos de selección y declaración, además de otros objetos que corresponden a la gramática de CSS.

Análisis de CSS.
Figura 12: Análisis de CSS

Orden de procesamiento de las secuencias de comandos y las hojas de estilo

Secuencias de comandos

El modelo de la Web es síncrono. Los autores esperan que las secuencias de comandos se analicen y se ejecuten de inmediato cuando el analizador llega a una etiqueta <script>. El análisis del documento se detiene hasta que se ejecute la secuencia de comandos. Si la secuencia de comandos es externa, primero se debe recuperar el recurso de la red. Esto también se hace de forma síncrona, y el análisis se detiene hasta que se recupera el recurso. Este fue el modelo durante muchos años y también está especificado en las especificaciones HTML4 y 5. Los autores pueden agregar el atributo "defer" a una secuencia de comandos, en cuyo caso no detendrá el análisis del documento y se ejecutará después de analizarlo. En HTML5 se agrega una opción para marcar la secuencia de comandos como asíncrona para que sea analizada y ejecutada por otro subproceso.

Análisis especulativo

WebKit y Firefox realizan esta optimización. Mientras se ejecutan las secuencias de comandos, otro subproceso analiza el resto del documento y determina qué otros recursos deben cargarse desde la red y los carga. De esta manera, se pueden cargar los recursos en conexiones paralelas y se mejora la velocidad general. Nota: El analizador especulativo solo analiza las referencias a recursos externos como secuencias de comandos externas, imágenes y hojas de estilo; no modifica el árbol del DOM; eso queda en manos del analizador principal.

Hojas de estilo

Por otro lado, las Hojas de estilo tienen un modelo diferente. Conceptualmente, parece que, dado que las hojas de estilo no cambian el árbol del DOM, no hay motivo para esperarlas y detener el análisis del documento. Sin embargo, hay un problema con las secuencias de comandos que solicitan información de estilo durante la etapa de análisis del documento. Si el estilo aún no se cargó ni analizó, la secuencia de comandos obtendrá respuestas incorrectas y, aparentemente, esto causó muchos problemas. Parece ser un caso límite, pero es bastante común. Firefox bloquea todas las secuencias de comandos cuando hay una hoja de estilo que aún se está cargando y analizando. WebKit bloquea las secuencias de comandos solo cuando intentan acceder a determinadas propiedades de estilo que pueden verse afectadas por las hojas de estilo descargadas.

Construcción del árbol de renderización

Mientras se construye el árbol del DOM, el navegador construye otro, el árbol de representación. Este árbol incluye elementos visuales en el orden en que se mostrarán. Es la representación visual del documento. El propósito de este árbol es permitir que se pinte el contenido en el orden correcto.

Firefox llama a los elementos del árbol de renderización “marcos”. WebKit usa el término procesador o objeto de renderización.

Un procesador sabe cómo diseñar y pintar a sí mismo y a sus elementos secundarios.

La clase RenderObject de WebKit, la clase base de los procesadores, tiene la siguiente definición:

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 procesador representa un área rectangular que, por lo general, corresponde al cuadro CSS de un nodo, como se describe en la especificación CSS2. Incluye información geométrica como el ancho, la altura y la posición.

El tipo de cuadro se ve afectado por el valor de visualización del atributo de estilo relevante para el nodo (consulta la sección computación de estilo). A continuación, se muestra el código de WebKit para decidir qué tipo de procesador se debe crear para un nodo del DOM, según el atributo de visualización:

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;
}

También se considera el tipo de elemento; por ejemplo, los controles de formularios y las tablas tienen marcos especiales.

En WebKit, si un elemento desea crear un procesador especial, anulará el método createRenderer(). Los procesadores apuntan a objetos de diseño que contienen información no geométrica.

Relación del árbol de renderización con el árbol del DOM

Los procesadores corresponden a los elementos del DOM, pero la relación no es de uno a uno. No se insertarán elementos del DOM no visuales en el árbol de renderización. Un ejemplo es el elemento "head". Además, los elementos cuyo valor de visualización se asignó a "none" no aparecerán en el árbol (mientras que los elementos con visibilidad "oculto" aparecerán en el árbol).

Hay elementos del DOM que corresponden a varios objetos visuales. Por lo general, estos son elementos con una estructura compleja que no se puede describir por un único rectángulo. Por ejemplo, el elemento "seleccionar" tiene tres representadores: uno para el área de visualización, uno para el cuadro de lista desplegable y otro para el botón. Además, cuando el texto se divide en varias líneas porque el ancho no es suficiente para una línea, las líneas nuevas se agregarán como procesadores adicionales.

Otro ejemplo de varios procesadores es el HTML con errores. Según las especificaciones de CSS, un elemento intercalado debe contener solo elementos de bloque o solo elementos intercalados. En el caso del contenido mixto, se crearán procesadores de bloques anónimos para unir los elementos intercalados.

Algunos objetos de renderización corresponden a un nodo del DOM, pero no se encuentran en el mismo lugar del árbol. Los números de punto flotante y los elementos absolutamente posicionados están fuera de flujo, se colocan en una parte diferente del árbol y se asignan al marco real. Un marco de marcador de posición es donde deberían haber estado.

El árbol de renderización y el árbol del DOM correspondiente.
Figura 13: El árbol de renderización y el árbol del DOM correspondiente. "Viewport" es el bloque contenedor inicial. En WebKit, será el objeto "RenderView".

El flujo de construcción del árbol

En Firefox, la presentación se registra como objeto de escucha de las actualizaciones del DOM. En la presentación, se delega la creación de marcos a FrameConstructor, y el constructor resuelve el estilo (consulta computación de estilo) y crea un marco.

En WebKit, el proceso de resolver el estilo y crear un renderizador se denomina "attachment". Cada nodo del DOM tiene un método de "conexión". El adjunto es síncrona; la inserción de nodos en el árbol del DOM llama al método "attach" del nodo nuevo.

El procesamiento de las etiquetas html y body da como resultado la construcción de la raíz del árbol de representación. El objeto de renderización raíz corresponde a lo que la especificación CSS llama el bloque contenedor: el bloque superior que contiene todos los demás bloques. Sus dimensiones son la viewport: las dimensiones del área de visualización de la ventana del navegador. Firefox la llama ViewPortFrame y WebKit la llama RenderView. Este es el objeto de renderización al que apunta el documento. El resto del árbol se construye como una inserción de nodos del DOM.

Consulta la especificación de CSS2 sobre el modelo de procesamiento.

Cálculo del diseño

Para compilar el árbol de renderización, es necesario calcular las propiedades visuales de cada objeto de renderización. Para ello, se calculan las propiedades de estilo de cada elemento.

El estilo incluye hojas de estilo de varios orígenes, elementos de estilo intercalados y propiedades visuales en el HTML (como la propiedad "bgcolor").Esta última acción se traduce a propiedades de estilo de CSS coincidentes.

Los orígenes de las hojas de estilo son las hojas de estilo predeterminadas del navegador, las hojas de estilo proporcionadas por el autor de la página y las hojas de estilo del usuario: estas son hojas de estilo proporcionadas por el usuario del navegador (los navegadores te permiten definir tus estilos favoritos). En Firefox, por ejemplo, esto se logra colocando una hoja de estilo en la carpeta "Perfil de Firefox").

El procesamiento de estilo presenta algunas dificultades:

  1. Los datos de estilo son una construcción muy grande que contiene las numerosas propiedades de estilo, lo que puede causar problemas de memoria.
  2. Encontrar las reglas de coincidencia para cada elemento puede causar problemas de rendimiento si no se optimiza. Recorrer toda la lista de reglas de cada elemento para encontrar coincidencias es una tarea pesada. Los selectores pueden tener una estructura compleja que puede provocar que el proceso de coincidencia comience en una ruta aparentemente prometedora que haya demostrado ser inútil y que se deba probar otra ruta.

    Por ejemplo, este selector compuesto:

    div div div div{
    ...
    }
    

    Significa que las reglas se aplican a un elemento <div> que es descendiente de 3 elementos div. Supongamos que deseas verificar si la regla se aplica a un elemento <div> determinado. Debes elegir una ruta determinada en el árbol para verificarla. Es posible que debas desviar el árbol de nodos hacia arriba solo para descubrir que solo hay dos div y que la regla no se aplica. Luego, debes probar otras rutas en el árbol.

  3. La aplicación de reglas implica reglas en cascada bastante complejas que definen la jerarquía de las reglas.

Veamos cómo los navegadores enfrentan estos problemas:

Uso compartido de datos de diseño

Los nodos de WebKit hacen referencia a objetos de estilo (RenderStyle). Los nodos pueden compartir estos objetos en algunas condiciones. Los nodos son hermanos o primos y:

  1. Los elementos deben estar en el mismo estado del mouse (p.ej., uno no puede estar en :hover mientras que el otro no)
  2. Ninguno de los elementos debe tener un ID
  3. Los nombres de las etiquetas deben coincidir
  4. Los atributos de clase deben coincidir
  5. El conjunto de atributos asignados debe ser idéntico
  6. Los estados de los vínculos deben coincidir
  7. Los estados del enfoque deben coincidir
  8. Ninguno de los elementos debe verse afectado por los selectores de atributos, cuando esto se define como una coincidencia de selector que utilice un selector de atributos en cualquier posición dentro del selector
  9. No debe haber un atributo de estilo intercalado en los elementos
  10. No debe haber ningún selector del mismo nivel en uso. WebCore simplemente arroja un interruptor global cuando se encuentra un selector del mismo nivel y, luego, inhabilita el uso compartido de estilos en todo el documento cuando está presente. Esto incluye el selector + y selectores como :first-child y :last-child.

Árbol de reglas de Firefox

Firefox tiene dos árboles adicionales para facilitar el procesamiento del estilo: el árbol de reglas y el árbol de contexto de estilo. WebKit también tiene objetos de estilo, pero no se almacenan en un árbol como el árbol de contexto de estilo, solo el nodo del DOM apunta a su estilo relevante.

Árbol de contexto de estilo de Firefox
Figura 14: Árbol de contexto de estilo de Firefox.

Los contextos de diseño contienen valores finales. Los valores se calculan aplicando todas las reglas de coincidencia en el orden correcto y realizando manipulaciones que los transforman de valores lógicos a concretos. Por ejemplo, si el valor lógico es un porcentaje de la pantalla, se calculará y transformará en unidades absolutas. La idea del árbol de reglas es realmente inteligente. Permite compartir estos valores entre nodos para evitar que se vuelvan a procesar. Esto también ahorra espacio.

Todas las reglas coincidentes se almacenan en un árbol. Los nodos inferiores de una ruta tienen mayor prioridad. El árbol contiene todas las rutas de acceso para las coincidencias de reglas que se encontraron. El almacenamiento de las reglas se realiza de forma diferida. El árbol no se calcula al principio para cada nodo, pero cuando es necesario calcular el estilo de un nodo, las rutas calculadas se agregan al árbol.

La idea es ver las rutas de los árboles como palabras en un léxico. Digamos que ya calculamos este árbol de reglas:

Árbol de reglas computadas
Figura 15: Árbol de reglas calculadas.

Supongamos que necesitamos hacer coincidir las reglas de otro elemento del árbol de contenido y descubrir que las reglas coincidentes (en el orden correcto) son B-E-I. Ya tenemos esta ruta en el árbol porque ya calculamos la ruta A-B-E-I-L. Ahora tendremos menos trabajo por hacer.

Veamos cómo el árbol nos ahorra trabajo.

División en structs

Los contextos de estilo se dividen en structs. Esos structs contienen información de estilo para una categoría determinada, como el borde o el color. Todas las propiedades de un struct son heredadas o no se heredan. Las propiedades heredadas son propiedades que, a menos que el elemento las defina, se heredan de su elemento superior. Las propiedades no heredadas (llamadas propiedades de "restablecimiento") usan valores predeterminados si no están definidas.

El árbol nos ayuda a almacenar en caché structs completas (que contienen los valores finales calculados) en el árbol. La idea es que, si el nodo inferior no proporcionó una definición para un struct, se puede usar un struct almacenado en caché en un nodo superior.

Calcula los contextos de estilo con el árbol de reglas

Cuando calculamos el contexto de estilo de un elemento determinado, primero calculamos una ruta de acceso en el árbol de reglas o usamos una existente. Luego, comenzamos a aplicar las reglas en la ruta de acceso para completar los structs en nuestro nuevo contexto de estilo. Comenzamos en el nodo inferior de la ruta, el que tiene la prioridad más alta (por lo general, el selector más específico) y recorremos el árbol hasta que nuestro struct esté completo. Si no hay ninguna especificación para el struct en ese nodo de regla, entonces podemos optimizar mucho. Subimos por el árbol hasta que encontremos un nodo que lo especifique por completo y lo dirija a él. Esa es la mejor optimización, se comparte toda la struct. Esto ahorra el procesamiento de los valores finales y la memoria.

Si encontramos definiciones parciales, subimos por el árbol hasta que se complete el struct.

Si no encontramos ninguna definición para nuestro struct, en caso de que el struct sea un tipo "heredado", apuntamos al struct de nuestro elemento superior en el árbol de contexto. En este caso, también logramos compartir los structs. Si se trata de una struct de restablecimiento, se usarán los valores predeterminados.

Si el nodo más específico agrega valores, entonces tenemos que hacer algunos cálculos adicionales para transformarlo en valores reales. Luego, almacenamos en caché el resultado en el nodo de árbol para que los elementos secundarios puedan usarlo.

En caso de que un elemento tenga un elemento del mismo nivel o un hermano que apunta al mismo nodo de árbol, se puede compartir entre ellos todo el contexto de estilo.

Veamos un ejemplo: Supongamos que tenemos 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>

Y las siguientes reglas:

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, supongamos que necesitamos completar solo dos structs: la struct de color y la struct de margen. La estructura de color contiene solo un miembro: el color La estructura de margen contiene los cuatro lados.

El árbol de reglas resultante se verá así (los nodos están marcados con el nombre del nodo, es decir, el número de la regla a la que apuntan):

El árbol de reglas
Figura 16: El árbol de reglas

El árbol de contexto se verá de la siguiente manera (nombre del nodo: nodo de regla al que apuntan):

El árbol de contexto
Figura 17: El árbol de contexto

Supongamos que analizamos el HTML y llegamos a la segunda etiqueta <div>. Debemos crear un contexto de estilo para este nodo y completar sus structs de estilo.

Haremos coincidir las reglas y descubriremos que las reglas de coincidencia para <div> son 1, 2 y 6. Esto significa que ya existe una ruta de acceso en el árbol que nuestro elemento puede usar y que solo debemos agregar otro nodo para la regla 6 (nodo F en el árbol de reglas).

Crearemos un contexto de estilo y lo pondremos en el árbol de contexto. El nuevo contexto de diseño dirigirá al nodo F del árbol de reglas.

Ahora debemos completar las structs de estilo. Para comenzar, completaremos el struct de margen. Dado que el último nodo de la regla (F) no agrega datos a la estructura de margen, podemos subir por el árbol hasta encontrar una estructura almacenada en caché calculada en una inserción de nodo anterior y usarla. Lo encontraremos en el nodo B, que es el nodo superior que especificó las reglas de margen.

Tenemos una definición para el struct color, por lo que no podemos usar uno almacenado en caché. Como el color tiene un solo atributo, no necesitamos subir en el árbol para rellenar otros atributos. Calcularemos el valor final (convertir la cadena a RGB, etc.) y almacenaremos en caché la struct calculada en este nodo.

El trabajo en el segundo elemento <span> es aún más fácil. Haremos coincidir las reglas y llegaremos a la conclusión de que apunta a la regla G, como el intervalo anterior. Como tenemos elementos del mismo nivel que apuntan al mismo nodo, podemos compartir todo el contexto de estilo y solo apuntar al contexto del intervalo anterior.

En el caso de los structs que contienen reglas heredadas del elemento superior, el almacenamiento en caché se realiza en el árbol de contexto (en realidad, la propiedad de color se hereda, pero Firefox la trata como restablecida y la almacena en caché en el árbol de reglas).

Por ejemplo, si agregamos reglas para las fuentes en un párrafo:

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

Entonces, el elemento de párrafo, que es un elemento secundario del elemento div en el árbol de contexto, podría haber compartido el mismo struct de fuente que su elemento superior. Esto sucede cuando no se especificaron reglas de fuente para el párrafo.

En WebKit, que no tiene un árbol de reglas, las declaraciones coincidentes se recorren cuatro veces. Se aplican las primeras propiedades de prioridad alta no importantes (propiedades que se deben aplicar primero porque otras dependen de ellas, como la pantalla); luego, las de prioridad alta son importantes, la de prioridad normal no es importante y, por último, las reglas de prioridad normal son importantes. Esto significa que las propiedades que aparecen varias veces se resolverán según el orden de cascada correcto. La última gana.

En resumen, compartir los objetos de estilo (en su totalidad o en algunas de las structs dentro de ellos) soluciona los problemas 1 y 3. El árbol de reglas de Firefox también ayuda a aplicar las propiedades en el orden correcto.

Manipula las reglas para una coincidencia fácil

Existen varias fuentes para las reglas de diseño:

  1. Reglas de CSS, ya sea en hojas de estilo externas o en elementos de diseño. css p {color: blue}
  2. Atributos de estilo intercalado, como html <p style="color: blue" />
  3. Atributos visuales HTML (que se asignan a reglas de estilo relevantes) html <p bgcolor="blue" /> Los dos últimos se pueden hacer coincidir fácilmente con el elemento, ya que es el propietario de los atributos de diseño, y los atributos HTML se pueden asignar usando el elemento como clave.

Como se mencionó anteriormente en el problema 2, la coincidencia de reglas de CSS puede ser más complicada. Para resolver la dificultad, se manipulan las reglas para facilitar el acceso.

Después de analizar la hoja de estilo, las reglas se agregan a uno de varios mapas hash, según el selector. Hay mapas por ID, nombre de clase, nombre de etiqueta y un mapa general para todo lo que no encaja en esas categorías. Si el selector es un ID, la regla se agregará al mapa de ID; si es una clase, se agregará al mapa de clases, etcétera.

Esta manipulación facilita mucho la coincidencia de reglas. No es necesario buscar en cada declaración, ya que podemos extraer de los mapas las reglas relevantes para un elemento. Esta optimización elimina más del 95% de las reglas, de modo que ni siquiera es necesario considerarlas durante el proceso de segmentación(4.1).

Veamos, por ejemplo, las siguientes reglas de diseño:

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

La primera regla se insertará en el mapa de la clase. La segunda es al mapa de IDs y la tercera, al mapa de etiquetas.

Para el siguiente fragmento de HTML:

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

Primero, intentaremos encontrar reglas para el elemento p. El mapa de clases contendrá una clave "error" en la que se encuentra la regla "p.error". El elemento div tendrá reglas relevantes en el mapa de ID (la clave es el ID) y en el mapa de etiquetas. Lo único que queda por hacer es descubrir cuáles de las reglas extraídas por las claves realmente coinciden.

Por ejemplo, si la regla del elemento div era la siguiente:

table div {margin: 5px}

Se extraerá del mapa de etiquetas de todos modos porque la clave es el selector más a la derecha, pero no coincidiría con nuestro elemento div, que no tiene un elemento principal de tabla.

Tanto WebKit como Firefox hacen esta manipulación.

Orden en cascada de la hoja de estilo

El objeto de estilo tiene propiedades que corresponden a cada atributo visual (todos los atributos CSS, pero los más genéricos). Si ninguna de las reglas coincidentes define la propiedad, el objeto de diseño del elemento superior puede heredar algunas propiedades. Otras propiedades tienen valores predeterminados.

El problema comienza cuando hay más de una definición. Aquí viene el orden en cascada para resolverlo.

Una declaración de una propiedad de estilo puede aparecer en varias hojas de estilo y varias veces dentro de una de ellas. Esto significa que el orden de aplicación de las reglas es muy importante. Esto se conoce como el orden de "cascada". Según la especificación de CSS2, el orden en cascada es (de menor a mayor):

  1. Declaraciones del navegador
  2. Declaraciones normales del usuario
  3. Crea declaraciones normales
  4. Crea declaraciones importantes
  5. Declaraciones importantes de los usuarios

Las declaraciones del navegador son menos importantes y el usuario anula al autor solo si se marcó como importante. Las declaraciones con el mismo orden se ordenan por especificidad y, luego, según el orden en el que se especifican. Los atributos visuales HTML se traducen a declaraciones de CSS coincidentes . Se tratan como reglas de autor con prioridad baja.

Especificidad

La especificación de CSS2 define la especificidad del selector de la siguiente manera:

  1. recuento 1 si la declaración de la que proviene es un atributo 'style' en lugar de una regla con un selector, de lo contrario, es 0 (= a)
  2. contar la cantidad de atributos de ID en el selector (= b)
  3. contar la cantidad de otros atributos y seudoclases en el selector (= c)
  4. Contar la cantidad de nombres de elementos y seudoelementos en el selector (= d)

La concatenación de los cuatro números a-b-c-d (en un sistema de números con una base grande) proporciona la especificidad.

La base numérica que debes usar se define según el recuento más alto que tengas en una de las categorías.

Por ejemplo, si a=14, puedes usar una base hexadecimal. En el caso improbable en el que a=17 necesitarás una base numérica de 17 dígitos. La situación posterior puede ocurrir con un selector como este: html body div div p... (17 etiquetas en el selector... no es muy probable).

Estos son algunos ejemplos:

 *             {}  /* 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 */

Ordena las reglas

Una vez que coinciden las reglas, se ordenan según las reglas de la cascada. WebKit usa la ordenación por burbujas para las listas pequeñas y la ordenación por combinación para las listas grandes. WebKit anula el operador > para las reglas a fin de implementar la ordenación:

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;
}

Proceso gradual

WebKit usa un indicador que indica si se cargaron todas las hojas de estilo de nivel superior (incluidas @imports). Si el estilo no está completamente cargado cuando se adjunta, se usan marcadores de posición, se marcan en el documento y se vuelven a calcular una vez que se hayan cargado las hojas de estilo.

Diseño

Cuando se crea el renderizador y se agrega al árbol, no tiene posición ni tamaño. El cálculo de estos valores se denomina diseño o reprocesamiento.

HTML utiliza un modelo de diseño basado en flujo, lo que significa que la mayor parte del tiempo es posible calcular la geometría en un solo pase. Por lo general, los elementos más adelante "en el flujo" no afectan la geometría de los elementos que están "en el flujo" anteriormente, por lo que el diseño puede continuar de izquierda a derecha, de arriba a abajo en el documento. Hay excepciones; por ejemplo, las tablas HTML pueden requerir más de un pase.

El sistema de coordenadas está relacionado con el marco raíz. Se usan las coordenadas izquierda y superior.

El diseño es un proceso recursivo. Comienza con el procesador raíz, que corresponde al elemento <html> del documento HTML. El diseño continúa de manera recursiva en toda la jerarquía de fotogramas o parte de ella, y calcula la información geométrica para cada procesador que la requiera.

La posición del procesador raíz es 0,0 y sus dimensiones son el viewport, la parte visible de la ventana del navegador.

Todos los procesadores tienen un método de “diseño” o “reprocesamiento”, cada uno invoca el método de diseño de los elementos secundarios que necesitan diseño.

Sistema de bits sucios

Para no hacer un diseño completo para cada pequeño cambio, los navegadores usan un sistema de “bit sucio”. Si un procesador se modifica o agrega, se marca a sí mismo y a sus elementos secundarios como "sucios" (necesitan diseño).

Hay dos marcas: "sucio" e "niños sucios", lo que significa que, aunque el procesador sea correcto, tiene al menos un elemento secundario que necesita un diseño.

Diseño incremental y global

El diseño se puede activar en todo el árbol de representación; este es un diseño “global”. Esto puede suceder como resultado de lo siguiente:

  1. Es un cambio de estilo global que afecta a todos los procesadores, como un cambio de tamaño de fuente.
  2. Como resultado del cambio de tamaño de una pantalla

El diseño puede ser incremental, solo se distribuirán los procesadores sucios (esto puede causar daños que requerirán diseños adicionales).

El diseño incremental se activa (de forma asíncrona) cuando los procesadores están inactivos. Por ejemplo, cuando se agregan nuevos procesadores al árbol de renderización después de que proviene contenido adicional de la red y se agrega al árbol del DOM.

Diseño incremental.
Figura 18: Diseño incremental: Solo se disponen los procesadores sucios y sus elementos secundarios

Diseño asíncrono y síncrono

El diseño incremental se realiza de forma asíncrona. Firefox pone en cola los "comandos de reprocesamiento" para los diseños incrementales, y un programador activa la ejecución por lotes de estos comandos. WebKit también tiene un temporizador que ejecuta un diseño incremental: se recorre el árbol y los renderizadores "sucios" se eliminan.

Las secuencias de comandos que solicitan información de estilo, como "offsetHeight" pueden activar el diseño incremental de manera síncrona.

El diseño global suele activarse de forma síncrona.

A veces, el diseño se activa como una devolución de llamada después de un diseño inicial porque algunos atributos, como la posición de desplazamiento, cambiaron.

Optimizaciones

Cuando un diseño se activa mediante un "cambio de tamaño" o un cambio en la posición del renderizador(y no en el tamaño), los tamaños de los renderizados se toman de una caché y no se vuelven a calcular...

En algunos casos, solo se modifica un subárbol y el diseño no comienza desde la raíz. Esto puede ocurrir en casos en los que el cambio es local y no afecta a su entorno, como cuando se inserta texto en campos de texto (de lo contrario, cada combinación de teclas activaría un diseño a partir de la raíz).

El proceso de diseño

El diseño generalmente tiene el siguiente patrón:

  1. El procesador superior determina su propio ancho.
  2. El padre o la madre revisa los elementos secundarios y hace lo siguiente:
    1. Coloca el procesador secundario (configura su x e y).
    2. Llama al diseño secundario si es necesario (está sucio, está en un diseño global o por alguna otra razón) que calcula la altura del niño.
  3. El elemento superior usa las alturas acumulativas de los elementos secundarios, así como las alturas de los márgenes y el padding para establecer su propia altura. Esto lo usará el elemento superior del procesador superior.
  4. Establece la parte sucia en falso.

Firefox utiliza un objeto "estado" (nsHTMLReflowState) como parámetro para el diseño (denominado "reprocesamiento"). Entre otros, el estado incluye el ancho de elementos superiores.

El resultado del diseño de Firefox es un objeto "metrics" (nsHTMLReflowMetrics). Incluirá la altura calculada por el renderizador.

Cálculo del ancho

El ancho del procesador se calcula con el ancho del bloque del contenedor, la propiedad "width" del estilo del procesador, los márgenes y los bordes.

Por ejemplo, el ancho del siguiente elemento div:

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

WebKit lo calcularía de la siguiente manera(clase RenderBox: método calcWidth):

  • El ancho del contenedor es el valor máximo de los contenedores availableWidth y 0. En este caso, availableWidth es el contentWidth, que se calcula de la siguiente manera:
clientWidth() - paddingLeft() - paddingRight()

clientWidth y clientHeight representan el interior de un objeto, sin incluir el borde ni la barra de desplazamiento.

  • El ancho de los elementos es el atributo de estilo "width". Para calcularlo como un valor absoluto, se calcula el porcentaje del ancho del contenedor.

  • Ahora se agregaron los bordes horizontales y los paddings.

Hasta ahora, este fue el cálculo del “ancho preferido”. Ahora, se calcularán el ancho mínimo y el máximo.

Si el ancho preferido es mayor que el ancho máximo, se usa el ancho máximo. Si es inferior al ancho mínimo (la unidad más pequeña que no se puede romper), se usa el ancho mínimo.

Los valores se almacenan en caché en caso de que se necesite un diseño, pero el ancho no cambia.

Saltos de línea

Cuando un procesador en medio de un diseño decide que debe fallar, este se detiene y se propaga al elemento superior del diseño que debe estar dañado. El elemento superior crea los procesadores adicionales y llama al diseño en ellos.

Pintura

En la etapa de pintura, se recorre el árbol de representación y se llama al método "Paint()" del renderizador para mostrar contenido en la pantalla. La pintura utiliza el componente de infraestructura de la IU.

Global e incremental

Al igual que el diseño, la pintura también puede ser global (todo el árbol está pintado) o incremental. En la pintura incremental, algunos de los renderizadores cambian de una manera que no afecta a todo el árbol. El renderizador modificado invalida su rectángulo en la pantalla. Esto hace que el SO lo vea como una “región sucia” y genere un evento de “pintura”. El SO lo hace de forma inteligente y une varias regiones en una. En Chrome, es más complicado porque el procesador está en un proceso diferente que el proceso principal. Chrome simula el comportamiento del SO hasta cierto punto. La presentación escucha estos eventos y delega el mensaje a la raíz de renderización. El árbol se recorre hasta que se alcanza el procesador correspondiente. Se volverá a pintar a sí misma (y, por lo general, a sus elementos secundarios).

El orden de las pinturas

CSS2 define el orden del proceso de pintura. En realidad, este es el orden en el que se apilan los elementos en los contextos de apilamiento. Este orden afecta la pintura, ya que las pilas están pintadas de atrás hacia adelante. El orden de apilado de un procesador de bloques es el siguiente:

  1. background color
  2. imagen de fondo
  3. borde
  4. children
  5. descripción

Lista de visualización de Firefox

Firefox revisa el árbol de renderización y crea una lista de visualización para el rectangular pintado. Contiene los renderizadores relevantes para el rectangular, en el orden de pintura correcto (fondos de los renderizadores, bordes, etc.).

De esta manera, se debe recorrer el árbol una sola vez para volver a pintar, en lugar de varias veces: pintar todos los fondos, luego, todas las imágenes, todos los bordes, etcétera.

Firefox optimiza el proceso al no agregar elementos que estarán ocultos, como elementos completamente debajo de otros elementos opacos.

Almacenamiento rectangular de WebKit

Antes de volver a procesar la imagen, WebKit guarda el rectángulo anterior como mapa de bits. Luego, solo pinta el delta entre el rectángulo nuevo y el antiguo.

Cambios dinámicos

Los navegadores intentan realizar la cantidad mínima de acciones posibles en respuesta a un cambio. Por lo tanto, los cambios en el color de un elemento solo harán que se vuelva a pintar el elemento. Los cambios en la posición del elemento harán que se diseñe y se vuelva a pintar el elemento, sus elementos secundarios y, posiblemente, elementos del mismo nivel. Si agregas un nodo del DOM, el nodo se diseñará y volverá a pintar. Los cambios importantes, como aumentar el tamaño de fuente del elemento "html", invalidarán las memorias caché, y volverán a diseñar y a pintar todo el árbol.

Subprocesos del motor de renderización

El motor de renderización tiene un solo subproceso. Casi todo, excepto las operaciones de red, se realiza en un solo subproceso. En Firefox y Safari, este es el subproceso principal del navegador. En Chrome, es el subproceso principal del proceso de pestañas.

Las operaciones de red las pueden realizar varios subprocesos paralelos. La cantidad de conexiones paralelas es limitada (por lo general, de 2 a 6 conexiones).

Bucle de eventos

El subproceso principal del navegador es un bucle de eventos. Se trata de un bucle infinito que mantiene el proceso activo. Espera eventos (como los de diseño y pintura) y los procesa. Este es el código de Firefox para el bucle de eventos principal:

while (!mExiting)
    NS_ProcessNextEvent(thread);

Modelo visual CSS2

El lienzo

Según la especificación de CSS2, el término lienzo describe "el espacio en el que se renderiza la estructura de formato": donde el navegador pinta el contenido.

El lienzo es infinito para cada dimensión del espacio, pero los navegadores eligen un ancho inicial según las dimensiones del viewport.

Según www.w3.org/TR/CSS2/zindex.html, el lienzo es transparente si está contenido dentro de otro y recibe un color definido por el navegador si no lo es.

Modelo de caja de CSS

El modelo de caja de CSS describe los cuadros rectangulares que se generan para los elementos del árbol de documentos y se organizan según el modelo de formato visual.

Cada cuadro tiene un área de contenido (p.ej., texto, una imagen, etc.) y áreas circundantes opcionales de padding, borde y margen.

Modelo de caja CSS2
Figura 19: Modelo de caja CSS2

Cada nodo genera 0... en esos cuadros.

Todos los elementos tienen una propiedad de "mostrar" que determina el tipo de cuadro que se generará.

Ejemplos:

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

El valor predeterminado es intercalado, pero la hoja de estilo del navegador puede establecer otros valores predeterminados. Por ejemplo, la visualización predeterminada para el elemento "div" es el de bloque.

Puedes encontrar un ejemplo de hoja de estilo predeterminada aquí: www.w3.org/TR/CSS2/sample.html.

Esquema de posicionamiento

Existen tres esquemas:

  1. Normal: El objeto se posiciona según su lugar en el documento. Esto significa que su lugar en el árbol de renderización es como el lugar en el árbol del DOM y se presenta según su tipo de caja y sus dimensiones.
  2. Flotante: primero el objeto se distribuye como el flujo normal y, luego, se mueve lo más lejos posible o hacia la derecha
  3. Absoluto: El objeto se coloca en el árbol de renderización en un lugar diferente que en el árbol del DOM.

El esquema de posicionamiento se establece mediante la propiedad "position" y el atributo "float".

  • estáticos y relativos provocan un flujo normal
  • El posicionamiento absoluto y el fijo

En el posicionamiento estático no se define ninguna posición y se usa el posicionamiento predeterminado. En los otros esquemas, el autor especifica la posición: arriba, abajo, izquierda y derecha.

La forma en que se distribuye la caja se determina de la siguiente manera:

  • Tipo de caja
  • Dimensiones del cuadro
  • Esquema de posicionamiento
  • Información externa, como el tamaño de la imagen y de la pantalla

Tipos de cuadros

Cuadro de bloques: Forma un bloque que tiene su propio rectángulo en la ventana del navegador.

Caja de bloques.
Figura 20: Caja de bloques

Cuadro intercalado: No tiene su propio bloque, sino que está dentro de un bloque contenedor.

Cuadros intercalados
Figura 21: Cuadros intercalados

Los bloques se formatean verticalmente uno después del otro. Las líneas intercaladas se formatean horizontalmente.

Formato intercalado y bloqueado.
Figura 22: Formato intercalado y bloqueado

Los cuadros intercalados se colocan dentro de líneas o “cuadros de línea”. Las líneas son al menos tan altas como el cuadro más alto, pero pueden ser más altas cuando las cajas están alineadas como “modelo de referencia”, es decir, la parte inferior de un elemento está alineada en un punto de otra caja que no es la parte inferior. Si el ancho del contenedor no es suficiente, las líneas intercaladas se pondrán en varias líneas. Esto suele ser lo que sucede en un párrafo.

Líneas.
Figura 23: Líneas

Posicionamiento

Relativo

Posicionamiento relativo: Se posiciona como de costumbre y, luego, se mueve según el delta requerido.

Posicionamiento relativo.
Figura 24: Posicionamiento relativo

Anuncio flotante

El cuadro flotante se desplaza hacia la izquierda o la derecha de una línea. La característica interesante es que las otras cajas fluyen a su alrededor. El código HTML:

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

Se verá de la siguiente manera:

Flotante.
Figura 25: Número de punto flotante

Absoluta y fija

El diseño se define con exactitud, independientemente del flujo normal. El elemento no participa en el flujo normal. Las dimensiones son relativas al contenedor. En "Fijo", el contenedor es el viewport.

Posicionamiento fijo.
Figura 26: Posicionamiento fijo

Representación en capas

Esto se especifica mediante la propiedad de CSS del índice z. Representa la tercera dimensión del cuadro: su posición a lo largo del "eje z".

Las cajas se dividen en pilas (llamadas contextos de apilado). En cada pila, los elementos de la parte posterior se pintarán primero y los elementos frontales, en la parte superior, más cerca del usuario. En caso de superposición, el elemento principal ocultará el elemento anterior.

Las pilas se ordenan de acuerdo con la propiedad del índice z. Los cuadros con la propiedad "índice z" forman una pila local. El viewport tiene la pila externa.

Ejemplo:

<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>

El resultado será el siguiente:

Posicionamiento fijo.
Figura 27: Posicionamiento fijo

Si bien el elemento div rojo precede al verde en el lenguaje de marcado y se hubiera pintado antes en el flujo regular, la propiedad del índice z es mayor, de manera que está más adelante en la pila que contiene el cuadro raíz.

Recursos

  1. Arquitectura del navegador

    1. Grosskurth, Alan. Arquitectura de referencia para navegadores web (pdf)
    2. Gupta, Vineet. Cómo funcionan los navegadores - Parte 1 - Arquitectura
  2. Análisis

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (también conocido como el "libro Dragon"), Addison-Wesley, 1986
    2. Rick Jelliffe. The Bold and the Beautiful: dos nuevos borradores 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 (Google Tech Talk video)
    3. L. David Baron, motor de diseño de Mozilla
    4. L. David Baron, Documentación del sistema de estilos de Mozilla
    5. Chris Waterson, Notes on HTML Reflow
    6. Chris Waterson, Gecko Overview
    7. Alexander Larsson, The Life of an HTML HTTP Request (La vida de una solicitud HTTP HTTP).
  4. WebKit

    1. David Hyatt, Implementa CSS(parte 1).
    2. David Hyatt, Descripción general de WebCore
    3. David Hyatt, WebCore Rendering
    4. David Hyatt, The FOUC Problem
  5. Especificaciones del W3C

    1. Especificación de HTML 4.01
    2. Especificación de W3C HTML5
    3. Especificación de las hojas de estilo en cascada de nivel 2, revisión 1 (CSS 2.1)
  6. Instrucciones de compilación de los navegadores

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

Traducciones

Esta página se tradujo al japonés dos veces:

Puedes ver las traducciones alojadas de forma externa de coreano y turco.

¡Gracias a todos!