Cómo funcionan los navegadores

Tras bambalinas de los navegadores web modernos

Prefacio

Esta introducción integral sobre las operaciones internas de WebKit y Gecko es el resultado de una gran investigación realizada por el desarrollador israelí Tali Garsiel. Durante algunos años, revisó todos los datos publicados sobre los componentes internos de los navegadores y dedicó mucho tiempo a leer el código fuente de los navegadores web. Escribió lo siguiente:

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

Paul Irish, Relaciones con desarrolladores de Chrome

Introducción

Los navegadores web son el software más utilizado. En esta introducción, explicaré cómo funcionan en segundo plano. Veremos qué sucede cuando escribes google.com en la barra de direcciones hasta que ves la página de Google en la pantalla del navegador.

Navegadores de los que hablaremos

En la actualidad, se usan cinco navegadores principales en las 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, UC Browser, los navegadores Nokia S40/S60 y Chrome. Todos ellos, excepto los navegadores Opera, se basan en WebKit. Daré ejemplos de los navegadores de código abierto Firefox y Chrome, y Safari (que es parcialmente 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 navegadores para computadoras. En dispositivos móviles, el navegador de 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. Por lo general, el recurso es 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 con un URI (identificador uniforme de recursos).

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

Las interfaces de usuario de los navegadores tienen mucho en común entre sí. Entre los elementos comunes de la interfaz de usuario, se incluyen los siguientes:

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

Curiosamente, la interfaz de usuario del navegador no se especifica en ninguna especificación formal, sino que proviene de las prácticas recomendadas que se han ido desarrollando a lo largo de los años y de la imitación de los navegadores entre sí. La especificación de HTML5 no define los elementos de la IU que debe tener un navegador, pero enumera algunos elementos comunes. Entre ellas, se encuentran la barra de direcciones, la barra de estado y la barra de herramientas. Por supuesto, hay 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. La interfaz de usuario: Incluye la barra de direcciones, los botones de retroceso y avance, el menú de favoritos, etcétera. Es decir, todas las partes de la pantalla del navegador, excepto la ventana en la que se ve la página solicitada.
  2. El motor del navegador: Organiza las acciones entre la IU y el motor de renderización.
  3. El 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 el HTML y el 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 la plataforma. En segundo plano, 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 de forma local, como las 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 para cada pestaña. Cada pestaña se ejecuta en un proceso independiente.

Motores de renderización

La responsabilidad del motor de renderización es, bueno… renderizar, 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 a través de complementos o extensiones, por ejemplo, documentos PDF con un complemento de visor de PDF. Sin embargo, en este capítulo nos enfocaremos en el caso de uso principal: mostrar HTML y las imágenes con formato de CSS.

Los diferentes navegadores usan distintos motores de renderización: Internet Explorer usa Trident, Firefox usa Gecko y Safari usa WebKit. Chrome y Opera (desde la versión 15) usan Blink, una bifurcación de WebKit.

WebKit es un motor de renderización de código abierto que comenzó como un motor para la plataforma Linux y que Apple modificó para que fuera compatible con 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 hará en fragmentos de 8 kB.

Después de eso, 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. La información de diseño junto con las instrucciones visuales en el HTML se usarán 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 mostrarse en la pantalla.

Después de la construcción del árbol de renderización, este pasa por un proceso de "diseño". Esto significa darle a cada nodo las coordenadas exactas en las que 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 comprender 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á hasta que se analice todo el HTML antes de comenzar a compilar y diseñar el árbol de renderización. Se analizarán y mostrarán partes del contenido, mientras que el proceso continuará con el resto del contenido que sigue llegando de la red.

Ejemplos de flujos principales

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

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

Gecko llama al árbol de elementos con formato visual "árbol de marcos". Cada elemento es un fotograma. WebKit usa el término "árbol de renderización", que consta de "objetos de renderización". WebKit usa el término "diseño" para la ubicación de los elementos, mientras que Gecko lo llama "reflujo". "Adjunto" es el término que usa WebKit para conectar los nodos del DOM y la información visual para crear el árbol de renderización. Una diferencia menor no semántica es que Gecko tiene una capa adicional entre el HTML y el árbol del DOM. Se llama "receptor de contenido" y es una fábrica para crear elementos del DOM. Hablaremos de cada parte del flujo:

Análisis: General

Dado que el análisis es un proceso muy importante dentro del motor de renderización, lo analizaremos con un poco más de detalle. Comencemos con una breve 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, analizar la expresión 2 + 3 - 1 podría devolver este árbol:

Nodo del árbol de expresiones matemáticas.
Figura 5: Nodo de árbol de expresión matemática

Gramática

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

Combinación de analizador y lexer

La interpretación se puede separar en dos subprocesos: el análisis léxico y el análisis de sintaxis.

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 el lenguaje humano, consistirá en todas las palabras que aparecen en el diccionario para ese idioma.

El análisis de sintaxis es la aplicación de las reglas de sintaxis del lenguaje.

Por lo general, los analizadores dividen el trabajo entre dos componentes: el analizador léxico (a veces llamado tokenizador), que se encarga de dividir la entrada en tokens válidos, y el analizador sintáctico, que se encarga de construir el árbol de análisis sintáctico analizando la estructura del documento según las reglas de sintaxis del lenguaje.

El analizador léxico sabe cómo quitar caracteres irrelevantes, como espacios en blanco y saltos de línea.

Del documento de origen 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 le pedirá al lexer un token nuevo y tratará de hacerlo coincidir con una de las reglas de sintaxis. Si se cumple una regla, se agregará un nodo correspondiente al token al árbol de análisis y el analizador solicitará otro token.

Si no coincide ninguna regla, el analizador almacenará el token de forma interna y seguirá solicitando tokens hasta que se encuentre una regla que coincida con todos los tokens almacenados de forma interna. Si no se encuentra ninguna regla, el analizador arrojará 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: transformar el documento de entrada en 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, traduce el árbol 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 ver el proceso de análisis.

Sintaxis:

  1. Los componentes básicos de la sintaxis del lenguaje son las expresiones, los términos y las 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 de resta.
  5. Un término es un token de número entero o una expresión

Analicemos la entrada 2 + 3 - 1.

La primera subcadena que coincide con una regla es 2: según la regla n° 5, es un término. La segunda coincidencia es 2 + 3: coincide con la tercera regla, que es un término seguido de una operación y, luego, de otro término. La siguiente coincidencia solo se encontrará al final de la entrada. 2 + 3 - 1 es una expresión porque ya sabemos que 2 + 3 es un término, por lo que tenemos un término seguido de una operación seguida de otro término. 2 + + no coincidirá con ninguna regla y, por lo tanto, es una entrada no válida.

Definiciones formales para el vocabulario y la sintaxis

El vocabulario suele expresarse 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 con 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 un lenguaje puede ser analizado por analizadores regulares si su gramática es una gramática libre de contexto. Una definición intuitiva de una gramática libre de contexto es una gramática que puede expresarse por completo en BNF. Para obtener una definición formal, consulta el artículo de Wikipedia sobre la gramática libre de contexto.

Tipos de analizadores

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

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

El analizador descendente 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 de la expresión evoluciona y coincide con las otras reglas, pero el punto de partida es la regla de nivel más alto).

El analizador ascendente analizará la entrada hasta que se cumpla una regla. Luego, reemplazará la entrada coincidente por la regla. Esto continuará hasta el final de la entrada. La expresión que coincide parcialmente se coloca en la pila del analizador.

Pila Entrada
2 + 3 - 1
término + 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 por 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. Les proporcionas la gramática de tu lenguaje (su vocabulario y reglas de sintaxis) y generan un analizador que funciona. Crear un analizador requiere una comprensión profunda 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 analizador léxico y Bison para crear un analizador (es posible que los encuentres 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 de HTML

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

Gramática de HTML

El vocabulario y la sintaxis de HTML se definen en 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.

Lamentablemente, ninguno de los temas de analizadores convencionales se aplica a HTML (no los mencioné solo por diversión, sino que se usarán para analizar CSS y JavaScript). El HTML no se puede definir fácilmente con una gramática independiente del contexto que necesitan los analizadores.

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

Esto parece extraño a primera vista, ya que HTML es bastante similar a XML. Hay muchos analizadores de XML disponibles. Existe una variación XML de HTML, XHTML, así que ¿cuál es la gran diferencia?

La diferencia es que el enfoque de HTML es más "flexible": te permite omitir ciertas etiquetas (que luego se agregan de forma implícita) o, a veces, omitir etiquetas de inicio o de fin, etcétera. En general, es una sintaxis "suave", a diferencia de la sintaxis rígida y exigente de XML.

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

DTD de HTML

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

Existen algunas variaciones de la DTD. El modo estricto se ajusta únicamente a las especificaciones, pero otros modos admiten el lenguaje de marcado que usaban los navegadores en el pasado. El propósito es la retrocompatibilidad con el contenido más antiguo. La DTD estricta actual se encuentra aquí: www.w3.org/TR/html4/strict.dtd

DOM

El árbol de salida (el "árbol de análisis") es un árbol de nodos de elementos y atributos del DOM. DOM es la sigla de Modelo de objetos del documento. Es la representación de objetos del documento HTML y la interfaz de los elementos HTML para el mundo exterior, como JavaScript.

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

El DOM tiene una relación casi de 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 en el 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, el DOM lo especifica la organización W3C. 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. Las definiciones de HTML se pueden encontrar 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 a que el árbol se construye con elementos que implementan una de las interfaces del DOM. Los navegadores usan implementaciones concretas que tienen otros atributos que el navegador usa de forma interna.

El algoritmo de análisis

Como vimos en las secciones anteriores, el HTML no se puede analizar con los analizadores descendentes o ascendentes habituales.

Estos son los motivos:

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

Como no pueden usar las técnicas de análisis habituales, los navegadores crean analizadores personalizados para analizar HTML.

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

La asignación de token es el análisis léxico, que analiza la entrada en tokens. Entre los tokens HTML, se encuentran 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 de árbol y consume el siguiente carácter 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 de 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 se ve influenciada por el estado actual de asignación de token y por el estado de construcción del árbol. Esto significa que el mismo carácter consumido producirá resultados diferentes para el siguiente estado correcto, según el estado actual. El algoritmo es demasiado complejo para describirlo por completo, por lo que veremos un ejemplo sencillo que nos ayudará a comprender el principio.

Ejemplo básico: Se asignan tokens al siguiente código 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". Consumir un carácter a-z provoca la creación de un "token de etiqueta de inicio", y el estado cambia a "Estado del nombre de la 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 un token de html.

Cuando se alcanza la etiqueta >, se emite el token actual y el estado vuelve a cambiar a "Data state". La etiqueta <body> se tratará con los mismos pasos. Hasta ahora, se emitieron las etiquetas html y body. Ahora volvemos al "Estado de los datos". Consumir el carácter H de Hello world provocará la creación y emisión de un token de carácter, esto continúa hasta que se alcanza el < de </body>. Emitiremos un token de carácter para cada carácter de Hello world.

Ahora volvemos al "Estado de apertura de la etiqueta". Consumir el siguiente / de entrada provocará la creación de un end tag token y un cambio al "estado de nombre de etiqueta". Nuevamente, nos quedamos en este estado hasta que llegamos a >.Luego, se emitirá el nuevo token de etiqueta y volveremos al "estado de datos". La entrada </html> se tratará como en el caso anterior.

Asigna tokens a la entrada de ejemplo
Figura 10: Se asignan tokens a 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 su raíz y se le agregarán elementos. El constructor de árboles procesará cada nodo que emita el tokenizador. Para cada token, la especificación define qué elemento del DOM es relevante para él y se creará para este token. El elemento se agrega al árbol del DOM y también a la pila de elementos abiertos. Esta pila se usa para corregir discrepancias de anidamiento y etiquetas sin cerrar. 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 del árbol para la entrada de ejemplo:

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

La entrada de 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". Recibir el token "html" provocará un cambio al modo "before html" y un reprocesamiento del token 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". Luego, se recibe el token "body". Se creará un HTMLHeadElement de forma implícita, aunque no tengamos un token "head", y se agregará al árbol.

Ahora pasamos al modo "in head" y, luego, a "after head". Se vuelve a procesar el token del cuerpo, se crea y se inserta un HTMLBodyElement, y el modo se transfiere 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 demás 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". La recepción del token de fin de archivo finalizará el análisis.

Construcción de árbol del 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 que se analice el documento. Luego, el estado del documento se establecerá en "complete" y se activará un evento "load".

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

Tolerancia a errores de los navegadores

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

Por ejemplo, observa este código HTML:

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

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

El manejo de errores es bastante coherente en los navegadores, pero, sorprendentemente, no forma parte de las especificaciones de HTML. Al igual que los marcadores y los botones Atrás y Adelante, es algo que se desarrolló en los navegadores con el paso de los años. Existen construcciones HTML no válidas conocidas que se repiten en muchos sitios, y los navegadores intentan corregirlas de una manera que sea compatible con otros navegadores.

La especificación de HTML5 define algunos de estos requisitos. (WebKit resume esto de forma clara en el comentario al principio de la clase del analizador de HTML).

El analizador analiza la entrada con asignación de token en el documento y compila el árbol de documentos. Si el documento tiene el formato correcto, analizarlo es sencillo.

Lamentablemente, tenemos que controlar muchos documentos HTML que no están bien formados, por lo que el analizador debe ser tolerante con los errores.

Debemos encargarnos 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 agregarlo después.
  2. No se nos permite agregar el elemento directamente. Podría ser que la persona que escribió el documento olvidó alguna etiqueta intermedia (o que la etiqueta intermedia es opcional). Este podría ser el caso de las siguientes etiquetas: HTML, HEAD, BODY, TBODY, TR, TD y LI (¿olvidé alguna?).
  3. Queremos agregar un elemento de bloque dentro de un elemento intercalado. Cierra todos los elementos intercalados hasta el siguiente elemento de bloque superior.
  4. Si esto no funciona, cierra elementos hasta que se nos permita agregar el elemento o 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 ser compatible con IE y Firefox, WebKit trata esto como <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 perdida

Una tabla aislada es una tabla dentro de otra tabla, pero no dentro de una celda de 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 hermanas:

<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 del elemento actual: extraerá la tabla interna de la pila de la tabla externa. Ahora las tablas serán hermanas.

Elementos de formulario anidados

En caso de que el usuario coloque un formulario dentro de otro, se ignorará el segundo.

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 de html o body mal ubicadas

Nuevamente, el comentario habla por sí mismo.

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

Por lo tanto, los autores web deben tener cuidado (a menos que quieran aparecer como ejemplo en un fragmento de código de tolerancia a errores de WebKit) y escribir HTML bien formado.

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 con los tipos de analizadores descritos en la introducción. De hecho, la especificación de CSS define la gramática léxica y sintáctica de CSS.

Veamos algunos ejemplos:

La gramática léxica (vocabulario) se define con 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 abreviatura de identificador, como un nombre de clase. "name" es un ID de elemento (al que se hace referencia con "#").

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 tiene 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 la siguiente definición:

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

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

Analizador de CSS de WebKit

WebKit usa los generadores de analizadores Flex y Bison para crear analizadores automáticamente a partir de los archivos de gramática de CSS. Como recordarás de la introducción al analizador, Bison crea un analizador ascendente de desplazamiento y reducción. Firefox usa un analizador descendente escrito manualmente. En ambos casos, cada archivo CSS se analiza en un objeto StyleSheet. Cada objeto contiene reglas de CSS. Los objetos de regla de CSS contienen objetos de selector y declaración, y otros objetos correspondientes a la gramática de CSS.

Analizando 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 ejecuten de inmediato cuando el analizador llega a una etiqueta <script>. El análisis del documento se detiene hasta que se ejecuta 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 se especifica en las especificaciones de HTML4 y 5. Los autores pueden agregar el atributo "defer" a una secuencia de comandos, en cuyo caso no se detendrá el análisis del documento y se ejecutará después de que se analice el documento. HTML5 agrega una opción para marcar la secuencia de comandos como asíncrona, de modo que un subproceso diferente la analice y ejecute.

Análisis especulativo

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

Hojas de estilo

Por otro lado, las hojas de estilo tienen un modelo diferente. Conceptualmente, parece que, como 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 carga ni se analiza, la secuencia de comandos obtendrá respuestas incorrectas y, al parecer, esto causó muchos problemas. Parece ser un caso extremo, 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 ciertas propiedades de estilo que pueden verse afectadas por hojas de estilo no cargadas.

Construcción del árbol de renderización

Mientras se construye el árbol del DOM, el navegador construye otro árbol, el de renderización. Este árbol es de 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 pinten los contenidos en el orden correcto.

Firefox llama "marcos" a los elementos del árbol de renderización. WebKit usa el término renderizador o renderizar objeto.

Un renderizador sabe cómo diseñar y pintar su propio contenido y el de sus elementos secundarios.

La clase RenderObject de WebKit, la clase base de los renderizadores, 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 renderizador representa un área rectangular que suele corresponder a la caja de CSS de un nodo, como se describe en la especificación de 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 "display" del atributo de estilo que es relevante para el nodo (consulta la sección Cálculo de estilo). Este es el código de WebKit para decidir qué tipo de renderizador 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 tiene en cuenta el tipo de elemento: por ejemplo, los controles de formulario y las tablas tienen marcos especiales.

En WebKit, si un elemento quiere crear un renderizador especial, anulará el método createRenderer(). Los renderizadores apuntan a objetos de estilo que contienen información no geométrica.

La relación del árbol de renderización con el árbol del DOM

Los renderizadores corresponden a elementos del DOM, pero la relación no es de uno a uno. Los elementos del DOM no visuales no se insertarán 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 "hidden" sí aparecerán en el árbol).

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

Otro ejemplo de varios renderizadores es el HTML dañado. Según la especificación de CSS, un elemento intercalado debe contener solo elementos de bloque o solo elementos intercalados. En el caso del contenido mixto, se crearán renderizadores de bloques anónimos para unir los elementos intercalados.

Algunos objetos de renderización corresponden a un nodo del DOM, pero no en el mismo lugar del árbol. Los elementos flotantes y con posicionamiento absoluto están fuera del 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. El "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 un objeto de escucha para las actualizaciones del DOM. La presentación delega la creación de marcos al FrameConstructor, y el constructor resuelve el estilo (consulta cálculo de estilo) y crea un marco.

En WebKit, el proceso de resolución del estilo y creación de un renderizador se denomina "adjunto". Cada nodo del DOM tiene un método "attach". La conexión 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 renderización. El objeto de renderización raíz corresponde a lo que la especificación de CSS llama el bloque contenedor: el bloque superior que contiene todos los demás bloques. Sus dimensiones son las del viewport: las dimensiones del área de visualización de la ventana del navegador. Firefox lo llama ViewPortFrame y WebKit lo 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 de estilo

Para compilar el árbol de renderización, se deben 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 se traduce en propiedades de estilo 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, que son las hojas de estilo proporcionadas por el usuario del navegador (los navegadores te permiten definir tus estilos favoritos). En Firefox, por ejemplo, esto se hace colocando una hoja de estilo en la carpeta "Perfil de Firefox").

El cálculo de estilo presenta algunas dificultades:

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

    Por ejemplo, este selector compuesto:

    div div div div{
    ...
    }
    

    Significa que las reglas se aplican a un <div> que es descendiente de 3 divs. Supongamos que quieres verificar si la regla se aplica a un elemento <div> determinado. Eliges una ruta determinada en el árbol para realizar la verificación. Es posible que debas recorrer el árbol de nodos hacia arriba solo para descubrir que hay solo dos divs y que la regla no se aplica. Luego, debes probar otras rutas en el árbol.

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

Veamos cómo los navegadores enfrentan estos problemas:

Cómo compartir datos de estilo

Los nodos de WebKit hacen referencia a objetos de estilo (RenderStyle). En algunas condiciones, los nodos pueden compartir estos objetos. Los nodos son hermanos o primos, y se cumple lo siguiente:

  1. Los elementos deben estar en el mismo estado del mouse (p. ej., uno no puede estar en :hover mientras que el otro no lo está).
  2. Ninguno de los elementos debe tener un ID.
  3. Los nombres de las etiquetas deben coincidir
  4. Los atributos de la 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 de enfoque deben coincidir
  8. Ninguno de los elementos debe verse afectado por los selectores de atributos, donde "afectado" se define como tener cualquier coincidencia de selector que use un selector de atributos en cualquier posición dentro del selector.
  9. No debe haber ningún atributo de estilo intercalado en los elementos.
  10. No debe haber ningún selector de hermanos en uso. WebCore simplemente activa un interruptor global cuando se encuentra cualquier selector de hermanos y, cuando están presentes, inhabilita el uso compartido de estilos para todo el documento. 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 cálculo de estilos: 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 estilo 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 valores 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 muy inteligente. Permite compartir estos valores entre nodos para evitar volver a calcularlos. Esto también ahorra espacio.

Todas las reglas que coinciden se almacenan en un árbol. Los nodos inferiores de una ruta de acceso 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, sino que, cada vez que se debe calcular un estilo de nodo, las rutas calculadas se agregan al árbol.

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

Árbol de reglas calculado
Figura 15: Árbol de reglas calculado.

Supongamos que necesitamos hacer coincidir las reglas para otro elemento en el árbol de contenido y descubrimos que las reglas que coinciden (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 se heredan o no se heredan. Las propiedades heredadas son propiedades que, a menos que las defina el elemento, se heredan de su elemento superior. Las propiedades no heredadas (llamadas propiedades de "restablecimiento") usan valores predeterminados si no se definen.

El árbol nos ayuda almacenando 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.

Cómo calcular los contextos de estilo con el árbol de reglas

Cuando se calcula el contexto de estilo para un elemento determinado, primero se calcula una ruta de acceso en el árbol de reglas o se usa 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 precedencia más alta (por lo general, el selector más específico), y recorremos el árbol hasta que se complete nuestra estructura. Si no hay una especificación para la estructura en ese nodo de regla, podemos optimizar en gran medida: subimos por el árbol hasta encontrar un nodo que la especifique por completo y apuntamos a él. Esa es la mejor optimización: se comparte toda la estructura. Esto ahorra el cálculo de los valores finales y la memoria.

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

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

Si el nodo más específico agrega valores, debemos realizar algunos cálculos adicionales para transformarlo en valores reales. Luego, almacenamos en caché el resultado en el nodo del árbol para que lo puedan usar los elementos secundarios.

En caso de que un elemento tenga un elemento hermano que apunte al mismo nodo de árbol, se puede compartir el contexto de estilo completo entre ellos.

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 las cosas, supongamos que solo necesitamos completar dos structs: el struct de color y el 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á de la siguiente manera (los nodos están marcados con el nombre del nodo: 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):

Es el árbol de contexto.
Figura 17: El árbol de contexto

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

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

Crearemos un contexto de estilo y lo colocaremos en el árbol de contexto. El nuevo contexto de estilo apuntará al nodo F en el árbol de reglas.

Ahora, debemos completar las estructuras de estilo. Comenzaremos completando la estructura de margen. Como el último nodo de regla (F) no se agrega al struct de margen, podemos subir por el árbol hasta encontrar un struct almacenado en caché que se haya calculado en una inserción de nodo anterior y usarlo. Lo encontraremos en el nodo B, que es el nodo superior que especificó las reglas de margen.

Tenemos una definición para la estructura de color, por lo que no podemos usar una estructura almacenada en caché. Como el color tiene un atributo, no necesitamos subir por el árbol para completar otros atributos. Calcularemos el valor final (convertiremos la cadena en RGB, etc.) y almacenaremos en caché la estructura 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 hermanos que apuntan al mismo nodo, podemos compartir todo el contexto de estilo y solo apuntar al contexto del intervalo anterior.

En el caso de las estructuras que contienen reglas heredadas del elemento superior, el almacenamiento en caché se realiza en el árbol de contexto (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}

Luego, el elemento de párrafo, que es un elemento secundario del div en el árbol de contexto, podría haber compartido la misma estructura de fuente que su elemento superior. Esto ocurre si 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. Primero, se aplican las propiedades de alta prioridad no importantes (propiedades que se deben aplicar primero porque otras dependen de ellas, como display), luego, las importantes de alta prioridad, después, las no importantes de prioridad normal y, por último, las reglas importantes de prioridad normal. Esto significa que las propiedades que aparecen varias veces se resolverán según el orden correcto de la cascada. La última gana.

En resumen, compartir los objetos de estilo (por completo o algunas de las estructuras que contienen) resuelve los problemas 1 y 3. El árbol de reglas de Firefox también ayuda a aplicar las propiedades en el orden correcto.

Manipular las reglas para que coincidan fácilmente

Existen varias fuentes para las reglas de estilo:

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

Como se indicó anteriormente en el problema n° 2, la coincidencia de reglas de CSS puede ser más compleja. 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, por nombre de clase, por nombre de etiqueta y un mapa general para todo lo que no se ajuste a 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.

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

Veamos, por ejemplo, las siguientes reglas de estilo:

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

La primera regla se insertará en el mapa de clases. El segundo, en el mapa de ID, y el tercero, en el mapa de etiquetas.

Para el siguiente fragmento 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 para "p.error". El elemento div tendrá reglas relevantes en el mapa de ID (la clave es el ID) y en el mapa de etiquetas. Por lo tanto, el único trabajo que queda es averiguar cuáles de las reglas que se extrajeron con las claves realmente coinciden.

Por ejemplo, si la regla para el div fuera la siguiente:

table div {margin: 5px}

Aún se extraerá del mapa de etiquetas, porque la clave es el selector que se encuentra más a la derecha, pero no coincidirá con nuestro elemento div, que no tiene un elemento table superior.

Tanto WebKit como Firefox realizan 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 de CSS, pero más genéricos). Si ninguna de las reglas coincidentes define la propiedad, el objeto de estilo del elemento superior puede heredar algunas propiedades. Otras propiedades tienen valores predeterminados.

El problema comienza cuando hay más de una definición. Aquí es donde entra en juego el orden de cascada para resolver el problema.

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

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

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

Especificidad

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

  1. cuenta 1 si la declaración de la que proviene es un atributo "style" en lugar de una regla con un selector, 0 de lo contrario (= a)
  2. Cuenta la cantidad de atributos de ID en el selector (= b).
  3. cuenta la cantidad de otros atributos y pseudoclases en el selector (= c)
  4. cuenta la cantidad de nombres de elementos y pseudoelementos en el selector (= d)

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

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

Por ejemplo, si a=14, puedes usar la base hexadecimal. En el caso improbable de que a=17, necesitarás una base numérica de 17 dígitos. La última situación puede ocurrir con un selector como este: html body div div p… (17 etiquetas en tu 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 */

Cómo ordenar las reglas

Después de que se encuentran coincidencias con las reglas, estas se ordenan según las reglas de cascada. WebKit usa el ordenamiento de burbuja para las listas pequeñas y el ordenamiento por mezcla para las grandes. WebKit implementa la ordenación anulando el operador > para las reglas:

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 marca si se cargaron todas las hojas de estilo de nivel superior (incluidas las @imports). Si el estilo no se carga por completo cuando se adjunta, se usan marcadores de posición y se marcan en el documento, y se volverán a calcular una vez que se carguen las hojas de estilo.

Diseño

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

HTML usa un modelo de diseño basado en el flujo, lo que significa que la mayoría de las veces es posible calcular la geometría en una sola pasada. Los elementos que se encuentran más adelante "en el flujo" no suelen afectar la geometría de los elementos que se encuentran antes "en el flujo", por lo que el diseño puede continuar de izquierda a derecha y de arriba abajo en el documento. Hay excepciones: por ejemplo, las tablas HTML pueden requerir más de un pase.

El sistema de coordenadas es relativo al marco raíz. Se usan las coordenadas superior e izquierda.

El diseño es un proceso recurrente. Comienza en el renderizador raíz, que corresponde al elemento <html> del documento HTML. El diseño continúa de forma recurrente a través de parte o de toda la jerarquía de marcos, calculando la información geométrica para cada renderizador que la requiera.

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

Todos los renderizadores tienen un método de "diseño" o "reflujo", y cada uno invoca el método de diseño de sus elementos secundarios que necesitan diseño.

Sistema de bits sucios

Para no realizar un diseño completo para cada pequeño cambio, los navegadores usan un sistema de "bit de modificación". Un renderizador que se cambia o se agrega se marca a sí mismo y a sus elementos secundarios como "sucios", es decir, que necesitan un diseño.

Hay dos marcas: "dirty" y "children are dirty", lo que significa que, aunque el renderizador en sí puede estar bien, tiene al menos un elemento secundario que necesita un diseño.

Diseño global e incremental

El diseño se puede activar en todo el árbol de renderización, lo que se conoce como diseño "global". Esto puede suceder por los siguientes motivos:

  1. Un cambio de estilo global que afecta a todos los renderizadores, como un cambio en el tamaño de la fuente
  2. Como resultado de un cambio de tamaño de la pantalla

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

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

Diseño incremental.
Figura 18: Diseño incremental: solo se diseñan los renderizadores 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 redistribución" 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 se diseñan los renderizadores "sucios".

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

Por lo general, el diseño global se activará 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 cambiaron algunos atributos, como la posición de desplazamiento.

Optimizaciones

Cuando un diseño se activa por 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 renderizadores 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 suceder en los casos en que el cambio es local y no afecta su entorno, como el texto insertado en los campos de texto (de lo contrario, cada pulsación de tecla activaría un diseño que comienza desde la raíz).

El proceso de diseño

Por lo general, el diseño tiene el siguiente patrón:

  1. El renderizador superior determina su propio ancho.
  2. La madre o el padre se acerca a los niños y hace lo siguiente:
    1. Coloca el renderizador secundario (establece sus valores x e y).
    2. Llama al diseño secundario si es necesario (si está sucio, si estamos en un diseño global o por algún otro motivo), lo que calcula la altura del elemento secundario.
  3. El elemento superior usa las alturas acumuladas de los elementos secundarios y las alturas de los márgenes y el padding para establecer su propia altura, que usará el elemento superior del renderizador superior.
  4. Establece su bit de modificación en falso.

Firefox usa un objeto de "estado" (nsHTMLReflowState) como parámetro para el diseño (denominado "reflujo"). Entre otros, el estado incluye el ancho del elemento superior.

El resultado del diseño de Firefox es un objeto de "métricas" (nsHTMLReflowMetrics). Contendrá la altura calculada del renderizador.

Cálculo del ancho

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

Por ejemplo, el ancho del siguiente div:

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

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

  • El ancho del contenedor es el máximo de los contenedores availableWidth y 0. En este caso, availableWidth es 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". Se calculará como un valor absoluto calculando el porcentaje del ancho del contenedor.

  • Ahora se agregaron los bordes y los paddings horizontales.

Hasta ahora, este era el cálculo del "ancho preferido". Ahora, se calcularán los anchos mínimo y máximo.

Si el ancho preferido es mayor que el ancho máximo, se usa el ancho máximo. Si es menor que el ancho mínimo (la unidad más pequeña que no se puede dividir), 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.

Salto de línea

Cuando un renderizador en medio de un diseño decide que necesita dividirse, se detiene y propaga al elemento superior del diseño que necesita dividirse. El elemento superior crea los renderizadores adicionales y llama a layout en ellos.

Pintura

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

Globales e incrementales

Al igual que el diseño, la pintura también puede ser global (se pinta todo el árbol) 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 sola. En Chrome, es más complicado porque el renderizador se encuentra en un proceso diferente al 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. Se recorre el árbol hasta llegar al renderizador pertinente. Se volverá a pintar a sí mismo (y, por lo general, a sus elementos secundarios).

El orden de pintura

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 se pintan de atrás hacia adelante. El orden de apilamiento de un renderizador de bloques es el siguiente:

  1. color de fondo
  2. imagen de fondo
  3. borde
  4. niños
  5. descripción

Lista de visualización de Firefox

Firefox revisa el árbol de renderización y compila una lista de visualización para el rectángulo pintado. Contiene los renderizadores relevantes para el rectángulo, en el orden de dibujo correcto (fondos de los renderizadores, luego bordes, etcétera).

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

Firefox optimiza el proceso, ya que no agrega elementos que se ocultarán, como los que están completamente debajo de otros elementos opacos.

Almacenamiento de rectángulos de WebKit

Antes de volver a pintar, WebKit guarda el rectángulo anterior como un mapa de bits. Luego, solo pinta el delta entre los rectángulos nuevos y los antiguos.

Cambios dinámicos

Los navegadores intentan realizar la menor cantidad posible de acciones en respuesta a un cambio. Por lo tanto, los cambios en el color de un elemento solo provocarán que se vuelva a pintar. Los cambios en la posición del elemento provocarán el diseño y el rediseño del elemento, sus elementos secundarios y, posiblemente, los hermanos. Agregar un nodo del DOM provocará el diseño y el rediseño del nodo. Los cambios importantes, como aumentar el tamaño de la fuente del elemento "html", provocarán la invalidación de las cachés, el cambio de diseño y el repintado de todo el árbol.

Los subprocesos del motor de renderización

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

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

Bucle de evento

El subproceso principal del navegador es un bucle de eventos. Es un bucle infinito que mantiene vivo el proceso. 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 de 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", es decir, 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 se le asigna un color definido por el navegador si no lo está.

Modelo de caja de CSS

El modelo de caja de CSS describe las cajas rectangulares que se generan para los elementos en el árbol del documento y que se disponen según el modelo de formato visual.

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

Modelo de caja de CSS2
Figura 19: Modelo de caja de CSS2

Cada nodo genera de 0 a n de estos cuadros.

Todos los elementos tienen una propiedad "display" 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 inline, pero la hoja de estilo del navegador puede establecer otros valores predeterminados. Por ejemplo, la visualización predeterminada para el elemento "div" es block.

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 su lugar en el árbol del DOM y se presenta según su tipo de cuadro y sus dimensiones.
  2. Flotante: El objeto se dispone primero como un flujo normal y, luego, se mueve lo más a la izquierda o a la derecha posible.
  3. Absoluto: El objeto se coloca en el árbol de renderización en un lugar diferente al del árbol del DOM.

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

  • static y relative causan un flujo normal
  • absolute y fixed causan un posicionamiento absoluto

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, derecha.

La forma en que se distribuye la caja se determina por lo siguiente:

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

Tipos de cajas

Caja de bloque: forma un bloque y tiene su propio rectángulo en la ventana del navegador.

Caja de bloque.
Figura 20: Caja de bloque

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

Cuadros intercalados.
Figura 21: Cajas intercaladas

Los bloques se formatean verticalmente uno tras otro. Los elementos intercalados tienen un formato horizontal.

Formato de bloque y de línea.
Figura 22: Formato de bloque y de intercalación

Las cajas intercaladas se colocan dentro de líneas o "cajas de línea". Las líneas son al menos tan altas como la caja más alta, pero pueden ser más altas cuando las cajas están alineadas con la "línea de base", lo que significa que la parte inferior de un elemento está alineada en un punto de otra caja que no sea la parte inferior. Si el ancho del contenedor no es suficiente, los elementos intercalados se colocarán en varias líneas. Esto es lo que suele suceder 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

Un cuadro flotante se desplaza hacia la izquierda o la derecha de una línea. La característica interesante es que las otras cajas fluyen alrededor de ella. 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:

Es un argumento flotante.
Figura 25: Float

Absoluto y fijo

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 fixed, el contenedor es el viewport.

Posicionamiento fijo
Figura 26: Posicionamiento fijo

Representación por capas

Esto se especifica con la propiedad CSS z-index. 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 apilamiento). En cada pila, los elementos de atrás se pintarán primero y los de adelante, encima, más cerca del usuario. En caso de superposición, el elemento que esté más adelante ocultará al que esté detrás.

Las pilas se ordenan según la propiedad z-index. Las cajas con la propiedad "z-index" forman una pila local. El viewport tiene la pila exterior.

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

Aunque el div rojo precede al verde en el lenguaje de marcado y se habría pintado antes en el flujo normal, la propiedad z-index es más alta, por lo que está más adelante en la pila que contiene la caja raíz.

Recursos

  1. Arquitectura del navegador

    1. Grosskurth, Alan. A Reference Architecture for Web Browsers (pdf)
    2. Gupta, Vineet. How Browsers Work - Part 1 - Architecture
  2. Análisis

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (también conocido como el "Libro del dragón"), Addison-Wesley, 1986
    2. Rick Jelliffe. The Bold and the Beautiful: two new drafts for 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 (video de charla técnica de Google)
    3. L. David Baron, Mozilla's Layout Engine
    4. L. David Baron, Mozilla Style System Documentation
    5. Chris Waterson, Notes on HTML Reflow
    6. Chris Waterson, Gecko Overview
    7. Alexander Larsson, The life of an HTML HTTP request
  4. WebKit

    1. David Hyatt, Implementing CSS(part 1)
    2. David Hyatt, An Overview of WebCore
    3. David Hyatt, WebCore Rendering
    4. David Hyatt, The FOUC Problem
  5. Especificaciones de W3C

    1. Especificación de HTML 4.01
    2. Especificación de HTML5 del W3C
    3. Especificación de la revisión 1 del nivel 2 de las hojas de estilo en cascada (CSS 2.1)
  6. Instrucciones de compilación de 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 en coreano y turco.

¡Gracias a todos!