Cómo funciona el navegador web moderno (parte 3)

Mariko Kosaka

Funcionamientos internos de un proceso del renderizador

Esta es la parte 3 de una serie de 4 blogs sobre el funcionamiento de los navegadores. Anteriormente, tratamos la arquitectura de varios procesos y el flujo de navegación. En esta publicación, veremos lo que ocurre dentro del proceso del renderizador.

El proceso del renderizador afecta muchos aspectos del rendimiento web. Dado que ocurren muchas cosas dentro del proceso del renderizador, esta publicación es solo una descripción general. Si deseas profundizar más, la sección Rendimiento de Fundamentos de la Web tiene muchos más recursos.

Los procesos del renderizador controlan el contenido web.

El proceso del renderizador es responsable de todo lo que sucede dentro de una pestaña. En un proceso del renderizador, el subproceso principal controla la mayor parte del código que envías al usuario. A veces, los subprocesos de trabajo controlan partes de tu JavaScript si usas un trabajador web o un service worker. Los subprocesos del compositor y de trama también se ejecutan dentro de los procesos de un procesador para renderizar una página de manera eficiente y fluida.

El trabajo principal del proceso del renderizador es convertir HTML, CSS y JavaScript en una página web con la que el usuario pueda interactuar.

Proceso del renderizador
Figura 1: Proceso del renderizador con un subproceso principal, subprocesos de trabajador, un subproceso compositor y un subproceso de trama dentro

Análisis

Construcción de un DOM

Cuando el proceso del renderizador recibe un mensaje de confirmación para una navegación y comienza a recibir datos HTML, el subproceso principal comienza a analizar la cadena de texto (HTML) y convertirla en un Model Objeto del documento (DOM).

El DOM es la representación interna de un navegador de la página, así como la estructura de datos y la API con las que el desarrollador web puede interactuar mediante JavaScript.

El análisis de un documento HTML y convertirlo en un DOM se define en función del estándar HTML. Es posible que hayas notado que ingresar HTML a un navegador nunca genera un error. Por ejemplo, si falta la etiqueta de cierre </p>, este es un código HTML válido. El lenguaje de marcado erróneo como Hi! <b>I'm <i>Chrome</b>!</i> (la etiqueta b se cierra antes de la etiqueta i) se trata como si escribieras Hi! <b>I'm <i>Chrome</i></b><i>!</i>. Esto se debe a que la especificación HTML está diseñada para manejar esos errores correctamente. Si te interesa saber cómo se hacen estas tareas, consulta la sección “Introducción al manejo de errores y casos extraños en el analizador” de las especificaciones de HTML.

Carga de subrecursos

Un sitio web suele utilizar recursos externos como imágenes, CSS y JavaScript. Esos archivos deben cargarse desde la red o la caché. El subproceso principal podría solicitarlos uno por uno a medida que los encuentra durante el análisis para compilar un DOM, pero, para acelerar el proceso, se ejecuta el "escáner de precarga" de forma simultánea. Si hay elementos como <img> o <link> en el documento HTML, el escáner de precarga mira los tokens que genera el analizador HTML y envía solicitudes al subproceso de red en el proceso del navegador.

DOM
Figura 2: El subproceso principal que analiza HTML y compila un árbol del DOM

JavaScript puede bloquear el análisis

Cuando el analizador de HTML encuentra una etiqueta <script>, pausa el análisis del documento HTML y tiene que cargar, analizar y ejecutar el código JavaScript. ¿Por qué sucede esto? Porque JavaScript puede cambiar la forma del documento usando elementos como document.write(), que cambia toda la estructura del DOM (la descripción general del modelo de análisis en la especificación HTML tiene un buen diagrama). Por eso, el analizador de HTML debe esperar a que se ejecute JavaScript para poder reanudar el análisis del documento HTML. Si quieres saber qué sucede en la ejecución de JavaScript, el equipo de V8 tiene charlas y entradas de blog sobre este tema.

Sugerencia al navegador sobre cómo quieres cargar recursos

Hay muchas maneras en que los desarrolladores web pueden enviar sugerencias al navegador para cargar los recursos de manera correcta. Si tu JavaScript no usa document.write(), puedes agregar los atributos async o defer a la etiqueta <script>. El navegador luego carga y ejecuta el código JavaScript de forma asíncrona y no bloquea el análisis. También puedes usar el módulo de JavaScript si corresponde. <link rel="preload"> es una forma de informar al navegador que el recurso es indispensable para la navegación actual y que deseas descargar lo antes posible. Puedes obtener más información al respecto en Priorización de recursos: Cómo lograr que el navegador te ayude.

Cálculo de estilo

No basta con tener un DOM para saber cómo se vería la página, ya que podemos diseñar elementos de página en CSS. El subproceso principal analiza CSS y determina el estilo computado de cada nodo del DOM. Esta es información sobre qué tipo de diseño se aplica a cada elemento según los selectores CSS. Puedes ver esta información en la sección computed de Herramientas para desarrolladores.

Estilo calculado
Figura 3: El subproceso principal que analiza CSS para agregar estilo calculado

Incluso si no proporcionas ninguna CSS, cada nodo del DOM tiene un estilo computado. La etiqueta <h1> se muestra más grande que la etiqueta <h2> y los márgenes están definidos para cada elemento. Esto se debe a que el navegador tiene una hoja de estilo predeterminada. Si deseas saber cómo es el CSS predeterminado de Chrome, puedes consultar el código fuente aquí.

Diseño

Ahora, el proceso del renderizador conoce la estructura de un documento y los estilos de cada nodo, pero eso no es suficiente para renderizar una página. Imagina que estás intentando describir una pintura a tu amigo por teléfono. "Hay un círculo rojo grande y un cuadrado azul pequeño" no es suficiente información para que tu amigo sepa cómo se vería exactamente la pintura.

juego de máquinas de fax
Figura 4: Una persona de pie frente a una pintura con una línea telefónica conectada a la otra persona

El diseño es un proceso para encontrar la geometría de los elementos. El subproceso principal recorre el DOM y los estilos calculados, y crea el árbol de diseño que contiene información como coordenadas x y y tamaños de cuadros delimitadores. El árbol de diseño puede ser similar al árbol del DOM, pero solo contiene información relacionada con lo que está visible en la página. Si se aplica display: none, ese elemento no forma parte del árbol de diseño (sin embargo, un elemento con visibility: hidden está en el árbol de diseño). De manera similar, si se aplica un seudoelemento con contenido como p::before{content:"Hi!"}, se incluirá en el árbol de diseño aunque no esté en el DOM.

layout
Figura 5: El subproceso principal que cubre el árbol del DOM con estilos calculados y produce un árbol de diseño
Figura 6: Diseño de cuadro de un párrafo que se mueve debido a un cambio de salto de línea

Determinar el diseño de una página es una tarea difícil. Incluso el diseño de página más simple, como un flujo de bloques de arriba abajo, debe tener en cuenta qué tan grande es la fuente y dónde separarla de líneas, ya que afectan el tamaño y la forma de un párrafo, lo que influye en la ubicación del siguiente párrafo.

CSS puede hacer que los elementos floten hacia un lado, enmascarar el elemento de desbordamiento y cambiar las direcciones de escritura. Como te imaginarás, esta etapa de diseño tiene una enorme tarea. En Chrome, todo un equipo de ingenieros trabaja en el diseño. Si quieres ver detalles de su trabajo, grabamos algunas charlas de la Conferencia BlinkOn, que son bastante interesantes de ver.

Pintura

juego de dibujo
Figura 7: Una persona frente a un lienzo sosteniendo un pincel y preguntándose si primero debe dibujar un círculo o un cuadrado

Tener un DOM, un estilo y un diseño aún no es suficiente para representar una página. Digamos que quieres reproducir una pintura. Conoces el tamaño, la forma y la ubicación de los elementos, pero aún debes juzgar el orden en que los pintas.

Por ejemplo, se podría establecer z-index para ciertos elementos; en ese caso, la pintura en el orden de los elementos escritos en el HTML dará como resultado una renderización incorrecta.

Error de índice z
Figura 8: Elementos de página que aparecen en el orden de lenguaje de marcado HTML, lo que genera una imagen renderizada incorrecta porque no se tuvo en cuenta el índice z

En este paso de pintura, el subproceso principal recorre el árbol de diseño para crear registros de pintura. El registro de pintura es una nota del proceso de pintura, como "primero fondo, luego texto y, luego, rectángulo". Si dibujaste en el elemento <canvas> usando JavaScript, es posible que este proceso te resulte conocido.

registros de pintura
Figura 9: Subproceso principal que recorre el árbol de diseño y produce registros de pintura

Actualizar la canalización de renderización es costoso.

Figura 10: Árboles de DOM y de estilo, diseño y pintura en el orden en que se generan

Lo más importante que debes comprender en la canalización de renderización es que, en cada paso, se usa el resultado de la operación anterior para crear datos nuevos. Por ejemplo, si algo cambia en el árbol de diseño, se debe volver a generar el orden de pintura para las partes afectadas del documento.

Si deseas animar elementos, el navegador debe ejecutar estas operaciones entre cada fotograma. La mayoría de nuestras pantallas actualizan la pantalla 60 veces por segundo (60 FPS). La animación será fluida para los ojos humanos cuando muevas objetos por la pantalla en cada fotograma. Sin embargo, si a la animación le faltan los fotogramas intermedios, la página aparecerá como "con bloqueos".

bloqueos de bloque por fotogramas faltantes
Figura 11: Fotogramas de animación en un cronograma

Incluso si tus operaciones de renderización mantienen el ritmo de la actualización de la pantalla, estos cálculos se ejecutan en el subproceso principal, por lo que podrían bloquearse cuando la aplicación ejecuta JavaScript.

bloqueo de jage por JavaScript
Figura 12: Fotogramas de animación en un cronograma, pero JavaScript bloquea un fotograma

Puedes dividir la operación de JavaScript en fragmentos pequeños y programar su ejecución en cada fotograma con requestAnimationFrame(). Para obtener más información sobre este tema, consulta Optimiza la ejecución de JavaScript. También puedes ejecutar JavaScript en Web Workers para evitar que se bloquee el subproceso principal.

solicitar marco de animación
Figura 13: Fragmentos más pequeños de JavaScript que se ejecutan en un cronograma con un marco de animación

Composición

¿Cómo dibujarías una página?

Figura 14: Animación de un proceso simple de trama

Ahora que el navegador conoce la estructura del documento, el estilo de cada elemento, la geometría de la página y el orden de la pintura, ¿cómo dibuja una página? Convertir esta información en píxeles en la pantalla se llama rasterización.

Quizás una forma básica de controlar esto sea a través de partes de trama dentro del viewport. Si un usuario se desplaza por la página, mueve el marco de trama y genera más tramas para completar las partes faltantes. Así es como Chrome manejaba la rasterización cuando se lanzó por primera vez. Sin embargo, el navegador moderno ejecuta un proceso más sofisticado llamado composición.

Qué es la composición

Figura 15: Animación del proceso de composición

La composición es una técnica para separar partes de una página en capas, rasterizarlas por separado y componer como una página en un subproceso separado llamado subproceso compositor. Si se produce el desplazamiento, dado que las capas ya están rasterizadas, todo lo que tiene que hacer es componer un nuevo marco. La animación se puede lograr de la misma manera moviendo capas y compuesta un nuevo fotograma.

Puedes ver cómo se divide tu sitio web en capas en Herramientas para desarrolladores con el panel Capas.

División en capas

Para averiguar qué elementos deben estar en qué capas, el subproceso principal recorre el árbol de diseño para crear el árbol de capas (esta parte se llama "Actualizar árbol de capas" en el panel de rendimiento de Herramientas para desarrolladores). Si ciertas partes de una página que deben ser capas separadas (como el menú lateral deslizable) no obtienen una, puedes sugerirle al navegador mediante el uso del atributo will-change en CSS.

árbol de capas
Figura 16: Subproceso principal que recorre el árbol de diseño que produce el árbol de capas

Es posible que te sientas tentado de agregar capas a cada elemento, pero la composición en una cantidad excesiva de capas podría provocar un funcionamiento más lento que la rasterización de pequeñas partes de una página en cada fotograma, por lo que es fundamental que midas el rendimiento de renderización de tu aplicación. Para obtener más información sobre el tema, consulta Limítate solo a las propiedades del compositor y administra el recuento de capas.

Trama y composición a partir del subproceso principal

Una vez que se crea el árbol de capas y se determinan los órdenes de pintura, el subproceso principal confirma esa información en el subproceso del compositor. A continuación, el subproceso del compositor rasteriza cada capa. Una capa puede ser grande, como toda la longitud de una página, de manera que el subproceso del compositor las divide en mosaicos y envía cada tarjeta a los subprocesos de trama. Los subprocesos de la trama rasterizan cada tarjeta y los almacenan en la memoria de la GPU.

trama
Figura 17: Subprocesos de trama que crean el mapa de bits de mosaicos y lo envían a la GPU

El subproceso compositor puede priorizar diferentes subprocesos de trama para que los elementos dentro del viewport (o cercanos) puedan generarse primero en trama. Una capa también tiene varios mosaicos para diferentes resoluciones a fin de controlar acciones como la acción de zoom.

Una vez que se generan las tarjetas, el subproceso del compositor reúne la información de la tarjeta llamada cuadros de dibujo para crear un marco del compositor.

Dibuja cuádruplos Contiene información como la ubicación de la tarjeta en la memoria y el lugar en la página para dibujar la tarjeta, teniendo en cuenta la composición de la página.
Marco del compositor Una colección de cuádruplos de dibujo que representan el marco de una página.

A continuación, se envía un marco del compositor al proceso del navegador a través de IPC. En este punto, se podría agregar otro fotograma del compositor desde el subproceso de IU para el cambio de la IU del navegador o desde otros procesos del procesador para las extensiones. Estos marcos del compositor se envían a la GPU para mostrarlos en una pantalla. Si llega un evento de desplazamiento, el subproceso del compositor crea otro marco del compositor que se enviará a la GPU.

compuesto
Figura 18: Subproceso del compositor que crea un marco compuesto. El marco se envía al proceso del navegador y, luego, a la GPU

El beneficio de la composición es que se realiza sin involucrar el subproceso principal. El subproceso compositor no necesita esperar el cálculo del estilo o la ejecución de JavaScript. Es por eso que las animaciones solo de composición se consideran las mejores para lograr un rendimiento fluido. Si es necesario volver a calcular el diseño o la pintura, se debe volver a calcular el subproceso principal.

Conclusión

En esta publicación, analizamos la canalización desde el análisis hasta la composición. Esperamos que ahora puedas leer más sobre la optimización del rendimiento de un sitio web.

En la siguiente y última publicación de esta serie, analizaremos con más detalle el subproceso del compositor y veremos lo que sucede cuando entran en juego entradas del usuario como mouse move y click.

¿Te gustó la publicación? Si tienes preguntas o sugerencias para la próxima publicación, comunícate con nosotros en la sección de comentarios o a @kosamari en Twitter.

A continuación: La entrada llega al compositor