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

Mariko Kosaka

Funcionamiento interno de un proceso del procesador

Esta es la tercera parte de una serie de blog que analiza cómo funcionan los navegadores. Anteriormente, abordamos la arquitectura de varios procesos y el flujo de navegación. En esta publicación, veremos lo que sucede dentro del proceso del procesador.

El proceso del procesador abarca muchos aspectos del rendimiento web. Como suceden muchas cosas dentro del proceso del procesador, esta publicación es solo una descripción general. Si quieres obtener más información, la sección Rendimiento de Fundamentos de la Web tiene muchos más recursos.

Los procesos del procesador controlan el contenido web

El proceso del renderizador se encarga 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. En ocasiones, los subprocesos de los trabajadores controlan partes de tu código JavaScript si usas un trabajador web o un service worker. Los subprocesos de la trama y el compositor también se ejecutan dentro de los procesos del procesador para renderizar una página de manera eficiente y fluida.

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

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

Análisis

Construcción de un DOM

Cuando el proceso del procesador 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 de Objeto D del documento (DOM).

El DOM es la representación interna de la página que realiza un navegador, así como la estructura de datos y la API con los que el desarrollador web puede interactuar a través de JavaScript.

El análisis de un documento HTML en un DOM se define mediante el estándar de HTML. Quizás hayas notado que proporcionar HTML a un navegador nunca arroja un error. Por ejemplo, la etiqueta de cierre </p> que falta 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 que 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 cosas, consulta la sección "Introducción al manejo de errores y casos extraños en el analizador" de las especificaciones de HTML.

Cargando subrecurso

Un sitio web generalmente utiliza 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, ya que los encuentra durante el análisis para compilar un DOM; sin embargo, para acelerar el proceso, se ejecuta en simultáneo el "analizador de precarga". Si hay elementos como <img> o <link> en el documento HTML, el escáner de precarga observa los tokens generados por el analizador de HTML y envía solicitudes al subproceso de red en el proceso del navegador.

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

JavaScript puede bloquear el análisis

Cuando el analizador de HTML encuentra una etiqueta <script>, detiene el análisis del documento HTML y tiene que 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 las especificaciones de HTML tiene un buen diagrama). Es por eso que el analizador de HTML debe esperar a que se ejecute JavaScript para poder reanudar el análisis del documento HTML. Si te interesa saber qué sucede en la ejecución de JavaScript, el equipo de V8 tiene charlas y entradas de blog sobre este tema.

Sugerencia sobre cómo deseas cargar los recursos en el navegador

Existen muchas maneras en las que los desarrolladores web pueden enviar sugerencias al navegador para cargar los recursos sin problemas. Si tu JavaScript no utiliza document.write(), puedes agregar los atributos 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 informarle al navegador que el recurso es definitivamente necesario para la navegación actual y que deseas descargarlo lo antes posible. Puede obtener más información al respecto en Prioridad de recursos: cómo hacer que el navegador lo ayude.

Cálculo de estilo

Tener un DOM no es suficiente para saber cómo se vería la página porque podemos aplicar estilos a los elementos de página en CSS. El subproceso principal analiza la CSS y determina el estilo computado para cada nodo del 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 Herramientas para desarrolladores.

Estilo calculado
Figura 3: El subproceso principal que analiza la CSS para agregar un 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 se definen 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 del documento y los estilos de cada nodo, pero eso no es suficiente para renderizar una página. Imagina que estás tratando de describirle una pintura a tu amigo por teléfono. "Hay un círculo rojo grande y un pequeño cuadrado azul" no es suficiente información para que tu amigo sepa cómo se vería exactamente la pintura.

juego de máquina de fax humana
Figura 4: Una persona de pie frente a un cuadro 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 computarizados, y crea el árbol de diseño que contiene información como coordenadas x y y tamaños de los cuadros delimitadores. El árbol de diseño puede tener una estructura similar a la del árbol del 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, hay un elemento con visibility: hidden en el árbol de diseño). Del mismo modo, si se aplica una seudoclase con contenido como p::before{content:"Hi!"}, se incluye en el árbol de diseño aunque no esté en el DOM.

diseño
Figura 5: El subproceso principal que analiza el árbol del DOM con estilos calculados y produce un árbol de diseño
Figura 6: Diseño de cuadro para un párrafo que se mueve debido al 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 se debe dividir su línea, ya que afectan el tamaño y la forma de un párrafo, lo que afecta dónde debe estar el siguiente párrafo.

CSS puede hacer que el elemento flote hacia un lado, enmascarar el elemento ampliado y cambiar las direcciones de escritura. Puedes imaginar que esta etapa de diseño tiene una tarea enorme. En Chrome, un equipo completo de ingenieros trabaja en el diseño. Si quieres ver los detalles de su trabajo, se grabaron algunas charlas de BlinkOn Conference y son bastante interesantes.

Pintura

juego de dibujo
Figura 7: Una persona frente a un lienzo sostiene un pincel y se pregunta si primero debería dibujar un círculo o un cuadrado

Tener un DOM, un estilo y un diseño sigue siendo no 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 en qué orden los pintas.

Por ejemplo, es posible establecer z-index para ciertos elementos; en ese caso, pintar el orden de los elementos escritos en el HTML dará como resultado una renderización incorrecta.

Error del índice z
Figura 8: Los elementos de página que aparecen en el orden de un lenguaje de marcado HTML, lo que da como resultado una imagen procesada de forma 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 "fondo primero, luego texto, luego rectángulo". Si dibujaste en el elemento <canvas> con JavaScript, es posible que este proceso te resulte familiar.

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

Actualizar la canalización de procesamiento es costoso

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

Lo más importante que debes comprender en la canalización de procesamiento 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 pedido de pintura para las partes afectadas del documento.

Si estás animando 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 pasa los fotogramas intermedios, la página se mostrará "con bloqueos".

bloqueo de captura por fotogramas faltantes
Figura 11: Fotogramas de animación en un cronograma

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ía bloquearse cuando tu aplicación ejecuta JavaScript.

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

Puedes dividir la operación de JavaScript en fragmentos pequeños y programar la ejecución en cada fotograma mediante requestAnimationFrame(). Para obtener más información sobre este tema, consulta Cómo optimizar la ejecución de JavaScript. También puedes ejecutar JavaScript en Web Workers para evitar bloquear 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 del proceso de trama simple

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 dibujará una página? Convertir esta información en píxeles en la pantalla se denomina rasterización.

Quizá una manera simple de manejar esto sería procesar partes del viewport en trama. Si un usuario se desplaza por la página, mueve el marco de trama y completa las partes faltantes con más tramas. Así es como Chrome se encargó de 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 componerlas como una página en un subproceso independiente denominado subproceso compositor. Si se produce un 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 compuestos un nuevo fotograma.

Puedes ver cómo se divide tu sitio web en capas en las Herramientas para desarrolladores mediante 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 denomina "Update Layer Tree" (Actualizar árbol de capas) en el panel de rendimiento de Herramientas para desarrolladores. Si algunas partes de una página que deberían ser capas independientes (como el menú lateral deslizante) no reciben una, puedes sugerirle al navegador una sugerencia mediante el atributo will-change en CSS.

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

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

Trama y composición fuera 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. Luego, el subproceso del compositor rasteriza cada capa. Una capa puede ser grande como la longitud total de una página, por lo que el subproceso del compositor las divide en mosaicos y envía cada uno a subprocesos de trama. Los subprocesos de trama generan la trama de cada mosaico y los almacenan en la memoria GPU.

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

El subproceso del compositor puede priorizar diferentes subprocesos de trama para que primero se puedan generar tramas de los elementos que están dentro del viewport (o los cercanos). Una capa también tiene varios mosaicos para diferentes resoluciones para controlar aspectos como la acción de acercar la imagen.

Una vez que se generan las tramas de los mosaicos, el subproceso del compositor recopila la información de estos, llamada cuadros de dibujo para crear un marco del compositor.

Dibujar cuadrantes Contiene información como la ubicación del mosaico en la memoria y en qué parte de la página se debe dibujar el mosaico, teniendo en cuenta la composición de la página.
Marco del compositor Una colección de cuadrantes de dibujo que representa un marco de una página.

Luego, se envía una trama del compositor al proceso del navegador a través de IPC. En este punto, se podría agregar otro marco 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 fotogramas del compositor se envían a la GPU para mostrarla en una pantalla. Si llega un evento de desplazamiento, el subproceso del compositor crea otro marco del compositor para enviarlo a la GPU.

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

La ventaja de la composición es que se realiza sin involucrar el subproceso principal. El subproceso del compositor no necesita esperar el cálculo del diseño ni la ejecución de JavaScript. Por eso, la composición solo de animaciones se considera la mejor opción para lograr un rendimiento uniforme. Si se debe volver a calcular el diseño o la pintura, se debe incluir el subproceso principal.

Conclusión

En esta publicación, observamos la renderización de la canalización, desde el análisis hasta la composición. Con suerte, ahora tendrás la capacidad de leer más sobre la optimización del rendimiento de un sitio web.

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

¿Te gustó la publicación? Si tienes alguna pregunta o sugerencia para la próxima publicación, será un placer escucharla en la sección de comentarios que aparece a continuación o en @kosamari en Twitter.

A continuación: Llega la entrada al compositor