Funcionamiento interno de un proceso de renderización
Esta es la parte 3 de una serie de 4 partes en el blog sobre cómo funcionan los navegadores. Anteriormente, analizamos la arquitectura de varios procesos y el flujo de navegación. En esta entrada, veremos qué sucede dentro del proceso del renderizador.
El proceso del renderizador abarca muchos aspectos del rendimiento web. Dado que sucede mucho dentro del proceso del renderizador, esta publicación es solo una descripción general. Si quieres profundizar, la sección Rendimiento de Conceptos básicos 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 de renderización, 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 código JavaScript si usas un trabajador web o un service worker. Los subprocesos de compositor y de trama también se ejecutan dentro de un proceso de renderización para renderizar una página de manera eficiente y fluida.
La tarea principal del proceso de renderización es convertir HTML, CSS y JavaScript en una página web con la que el usuario puede interactuar.

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 la convierte en un modelo de Documento Objeto Model (DOM).
El DOM es la representación interna de la página de un navegador, así como la estructura de datos y la API con las que el desarrollador web puede interactuar a través de JavaScript.
El estándar HTML define el análisis de un documento HTML en un DOM. Es posible que hayas notado que alimentar HTML a un navegador nunca arroja un error. Por ejemplo, una etiqueta </p>
de cierre faltante es un HTML válido. El marcado incorrecto, 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 controlar esos errores de forma fluida. Si te interesa saber cómo se hacen estas tareas, puedes leer la sección “An introduction to error handling and strange cases in the parser” de la especificación de HTML.
Carga de subrecursos
Por lo general, un sitio web usa 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 mientras analiza para compilar un DOM, pero para acelerar el proceso, el "escáner de carga previa" se ejecuta de forma simultánea.
Si hay elementos como <img>
o <link>
en el documento HTML, el escáner de carga previa observa los tokens que genera el analizador de HTML y envía solicitudes al subproceso de red en el proceso del navegador.

JavaScript puede bloquear el análisis.
Cuando el analizador de HTML encuentra una etiqueta <script>
, detiene el análisis del documento HTML y debe cargar, analizar y ejecutar el código JavaScript. ¿Por qué? Porque JavaScript puede cambiar la forma del documento con elementos como document.write()
, que cambia toda la estructura del DOM (la descripción general del modelo de análisis en la especificación de HTML tiene un buen diagrama). Por este motivo, el analizador de HTML debe esperar a que se ejecute JavaScript antes de poder reanudar el análisis del documento HTML. Si te interesa saber qué sucede durante la ejecución de JavaScript, el equipo de V8 tiene charlas y entradas de blog sobre este tema.
Sugerirle al navegador cómo quieres cargar los recursos
Los desarrolladores web pueden enviar sugerencias al navegador de muchas maneras para cargar recursos de forma correcta.
Si tu código JavaScript no usa document.write()
, puedes agregar el atributo async
o defer
a la etiqueta <script>
. Luego, el navegador 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 es adecuado. <link rel="preload">
es una forma de informar al navegador que el recurso es definitivamente necesario para la navegación actual y que deseas descargarlo lo antes posible. Puedes obtener más información sobre este tema en Priorización de recursos: cómo hacer que el navegador te ayude.
Cálculo de diseño
Tener un DOM no es suficiente para saber cómo se verá la página, ya que podemos aplicar diseño a los elementos de la página en CSS. El subproceso principal analiza el CSS y determina el estilo calculado para cada nodo DOM. Esta es información sobre el tipo de estilo que se aplica a cada elemento según los selectores CSS. Puedes ver esta información en la sección computed
de DevTools.

Incluso si no proporcionas ningún CSS, cada nodo DOM tiene un estilo calculado. La etiqueta <h1>
se muestra más grande que la etiqueta <h2>
y se definen márgenes para cada elemento. Esto se debe a que el navegador tiene una hoja de estilo predeterminada. Si quieres saber cómo es el CSS predeterminado de Chrome, puedes ver 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 intentas describirle un cuadro 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 el cuadro.

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 tiene información como las coordenadas x e y y los tamaños de los cuadros de límite. El árbol de diseño puede tener una estructura similar al árbol de DOM, pero solo contiene información relacionada con lo que es 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). Del mismo modo, si se aplica un pseudoelemento con contenido como p::before{content:"Hi!"}
, se incluye en el árbol de diseño, aunque no esté en el DOM.

Determinar el diseño de una página es una tarea desafiante. Incluso el diseño de página más simple, como un flujo de bloques de arriba abajo, debe tener en cuenta el tamaño de la fuente y dónde colocar los saltos de línea, ya que estos afectan el tamaño y la forma de un párrafo, lo que luego afecta la ubicación del siguiente párrafo.
CSS puede hacer que el elemento flote a un lado, oculte el elemento desbordado y cambie las direcciones de escritura. Como puedes imaginártelo, esta etapa de diseño tiene una tarea formidable. En Chrome, un equipo completo de ingenieros trabaja en el diseño. Si quieres ver detalles de su trabajo, algunas charlas de la conferencia BlinkOn están grabadas y son bastante interesantes.
Pintura

Tener un DOM, un estilo y un diseño aún no es suficiente para renderizar una página. Supongamos que quieres reproducir una pintura. Sabes el tamaño, la forma y la ubicación de los elementos, pero aún tienes que juzgar en qué orden los pintas.
Por ejemplo, z-index
se puede configurar para ciertos elementos. En ese caso, pintar en orden de los elementos escritos en el HTML generará una renderización incorrecta.

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 el fondo, luego el texto y, luego, el rectángulo". Si dibujaste en el elemento <canvas>
con JavaScript, es posible que este proceso te resulte familiar.

La actualización de la canalización de renderización es costosa.
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 animas 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 se verá fluida para los ojos humanos cuando muevas elementos por la pantalla en cada fotograma. Sin embargo, si la animación no incluye los fotogramas intermedios, la página se verá “inestable”.

Incluso si tus operaciones de renderización se mantienen al día con la actualización de la pantalla, estos cálculos se ejecutan en el subproceso principal, lo que significa que podrían bloquearse cuando tu aplicación ejecute JavaScript.

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

Composiciones
¿Cómo dibujarías una página?
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 pintura, ¿cómo dibuja una página? Convertir esta información en píxeles en la pantalla se denomina rasterización.
Quizás una forma ingenua de controlar esto sería rasterizar partes dentro del viewport. Si un usuario se desplaza por la página, mueve el marco rasterizado y rasteriza más para completar las partes que faltan. Esta es la forma en que Chrome controlaba 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
La composición es una técnica para separar partes de una página en capas, rasterizarlas por separado y, luego, componerlas como una página en un subproceso independiente llamado subproceso de compositor. Si se produce el desplazamiento, como las capas ya están rasterizadas, todo lo que tiene que hacer es combinar un nuevo fotograma. La animación se puede lograr de la misma manera moviendo capas y componiendo un nuevo fotograma.
Puedes ver cómo se divide tu sitio web en capas en DevTools con el panel Capas.
División en capas
Para saber 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 denomina "Update Layer Tree" en el panel de rendimiento de DevTools). Si ciertas partes de una página que deberían ser una capa independiente (como el menú lateral deslizante) no tienen una, puedes sugerirle al navegador que use el atributo will-change
en CSS.

Es posible que tengas la tentación de asignar capas a cada elemento, pero la composición de una cantidad excesiva de capas podría ralentizar la operación más que rasterizar 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 Usa solo propiedades del compositor y administra el recuento de capas.
Renderiza y combina fuera del subproceso principal
Una vez que se crea el árbol de capas y se determinan los pedidos de pintura, el subproceso principal confirma esa información en el subproceso del compositor. Luego, el subproceso del compositor rasteriza cada capa. Una capa podría ser grande, como el ancho completo de una página, por lo que el subproceso del compositor las divide en mosaicos y envía cada uno a los subprocesos de trama. Los subprocesos de trama rasterizan cada mosaico y los almacenan en la memoria de la GPU.

El subproceso del compositor puede priorizar diferentes subprocesos de renderización para que los elementos dentro del viewport (o cerca) se rendericen primero. Una capa también tiene varias divisiones para diferentes resoluciones para controlar elementos como la acción de acercar.
Una vez que se rasterizan las tarjetas, el subproceso del compositor recopila información de las tarjetas llamada draw quads para crear un marco de compositor.
Cómo dibujar cuadrángulos | Contiene información como la ubicación de la tarjeta en la memoria y dónde dibujarla en la página, teniendo en cuenta la composición de la página. |
Marco del compositor | Es una colección de cuadrángulos de dibujo que representa un marco de una página. |
Luego, se envía un fotograma del compositor al proceso del navegador a través de IPC. En este punto, se podría agregar otro marco de compositor desde el subproceso de IU para el cambio de la IU del navegador o desde otros procesos de renderización para extensiones. Estos fotogramas del compositor se envían a la GPU para que se muestren en una pantalla. Si llega un evento de desplazamiento, el subproceso del compositor crea otro fotograma del compositor para enviarlo a la GPU.

El beneficio de la composición es que se realiza sin involucrar el subproceso principal. El subproceso del compositor no necesita esperar el cálculo de estilo ni la ejecución de JavaScript. Es por eso que la composición solo de animaciones se considera la mejor para obtener un rendimiento fluido. Si se debe volver a calcular el diseño o la pintura, el subproceso principal debe estar involucrado.
Conclusión
En esta publicación, analizamos la canalización de renderización desde el análisis hasta la composición. Esperamos que ahora tengas más información para leer sobre la optimización del rendimiento de un sitio web.
En la próxima y última publicación de esta serie, analizaremos el subproceso del compositor con más detalle y veremos qué sucede cuando llega la entrada del usuario, como mouse move
y click
.
¿Te gustó la publicación? Si tienes alguna pregunta o sugerencia para la próxima publicación, nos encantaría que nos escribas en la sección de comentarios a continuación o en @kosamari en Twitter.