wandres.dev
LA RUTA CRÍTICA · Qué bloquea el primer pintado

De bytes a píxeles: la ruta crítica de renderizado

El modelo mental completo del camino que va del primer byte del HTML al primer píxel en pantalla, con las dependencias que lo hacen secuencial.

⏱ 18 min

La ruta crítica de renderizado es la secuencia de pasos que el navegador tiene que completar antes de poder pintar contenido. Conocerla con precisión es lo que convierte “la página tarda en aparecer” en “el CSSOM no puede completarse porque falta una hoja de estilos”. Es el modelo mental sobre el que se apoyan el resto de lecciones de este nivel y buena parte de las de optimización del LCP.

🎯 Al terminar esta lección sabrás
  • Enumerar las etapas del camino de bytes a píxeles y qué produce cada una.
  • Identificar qué etapas dependen de qué y por qué el camino es secuencial.
  • Definir con precisión qué es un recurso que bloquea el renderizado.
  • Localizar en un perfil real dónde se está deteniendo la ruta.

El diagrama completo

flowchart TB
A[Bytes del HTML] --> B[Tokens]
B --> C[Arbol DOM]
D[Bytes del CSS] --> E[Arbol CSSOM]
C --> F[Arbol de renderizado]
E --> F
F --> G[Layout y calculo de geometria]
G --> H[Paint y rasterizado]
H --> I[Composicion de capas]
I --> J[Primer pintado con contenido]
style C fill:#89b4fa,color:#11111b
style E fill:#89b4fa,color:#11111b
style F fill:#cba6f7,color:#11111b
style G fill:#f9e2af,color:#11111b
style J fill:#a6e3a1,color:#11111b

La estructura tiene dos ramas que confluyen. La rama del HTML produce el árbol DOM; la del CSS produce el CSSOM. Ninguna de las dos sirve de nada por separado: hace falta combinarlas para saber qué se pinta y con qué aspecto. Esa confluencia es la que explica por qué el CSS bloquea el renderizado aunque el HTML ya esté listo.

Etapa por etapa

De bytes a tokens. El navegador recibe el flujo de bytes, los interpreta según la codificación declarada y los convierte en tokens: etiquetas de apertura, de cierre, atributos, texto. Es un proceso incremental que ocurre conforme llegan los bytes, no al final.

Aquí hay un detalle con consecuencias: si la codificación no está declarada pronto, el navegador tiene que adivinarla, y si adivina mal, puede tener que descartar el trabajo hecho y volver a empezar. Por eso la etiqueta de juego de caracteres va lo primero en el <head>.

De tokens a DOM. Los tokens se ensamblan en un árbol de nodos con relaciones de padre e hijo. También es incremental: el DOM va creciendo conforme llega el HTML, y el navegador puede pintar con un DOM parcial.

De bytes a CSSOM. El CSS se parsea en un modelo de objetos con las reglas, sus selectores y sus valores. Aquí está la diferencia fundamental con el DOM: el CSSOM no es útil parcialmente. Una regla que llegue en el último byte de la última hoja de estilos puede cambiar el aspecto de cualquier elemento de la página, incluso de uno que ya se hubiera pintado. Como el navegador no puede saberlo por adelantado, tiene que esperar a tenerlo todo.

Esa asimetría es la razón técnica de que el CSS bloquee el renderizado y el HTML no.

Árbol de renderizado. Se combinan DOM y CSSOM para producir el árbol de lo que efectivamente se va a pintar. No es una copia del DOM: se excluyen los elementos que no se renderizan, como los que están dentro de <head> o los que tienen display: none. Y se incluyen cosas que no están en el DOM, como el contenido generado por pseudoelementos.

Ojo con la distinción: visibility: hidden entra en el árbol de renderizado, porque ocupa espacio; display: none no.

Layout. Se calcula la geometría de cada caja: posición y tamaño, en píxeles reales, resolviendo unidades relativas contra el tamaño de la ventana. Es la etapa que más escala con el tamaño del DOM, porque depende de las relaciones entre elementos.

Paint. Se convierte cada caja en órdenes de dibujo y se rasterizan en mapas de píxeles, típicamente por capas.

Composición. Las capas se combinan en el orden correcto para producir el fotograma final. Esta etapa puede ocurrir fuera del hilo principal, y esa es la razón por la que animar transform y opacity es barato: solo requiere recomponer.

ℹ️
El pipeline no es estrictamente lineal, y por eso hay tantas oportunidades de optimización

El navegador no ejecuta las etapas una vez y termina. Las repite continuamente conforme llega contenido, cambia el estado o el usuario interactúa, y salta las que puede: un cambio que solo afecta al color se resuelve en paint sin repetir el layout; un cambio que solo mueve una capa se resuelve en composición sin repetir el paint. Todo el trabajo de optimización de renderizado consiste en provocar la etapa más tardía posible.

Qué bloquea el renderizado

Un recurso bloquea el renderizado si el navegador no puede pintar hasta tenerlo. La lista es corta y no incluye lo que la gente cree.

Sí bloquean:

  • Las hojas de estilo referenciadas desde el HTML cuya condición de medio coincida con el dispositivo actual.
  • Los scripts síncronos que aparezcan antes del contenido, porque detienen el parser.
  • Los recursos marcados explícitamente como bloqueantes con el atributo correspondiente.

No bloquean:

  • Las imágenes. Nunca bloquean el renderizado. La página se pinta con el hueco de la imagen y esta aparece cuando llega. Este es el error de intuición más común del tema.
  • Los scripts con defer o async.
  • Las hojas de estilo cuya condición de medio no se cumpla.
  • Las fuentes web. No bloquean el renderizado de la página, aunque sí pueden bloquear el pintado del texto que las usa durante un periodo determinado por su configuración.

Distinguir estas dos listas resuelve una confusión frecuente: una página puede tener el primer pintado bloqueado durante dos segundos por un CSS de 300 KB mientras una imagen de 2 MB se descarga tranquilamente sin molestar a nadie.

Un matiz importante sobre los scripts: un script síncrono no solo bloquea el parser, también tiene que esperar al CSS. La razón es que el script puede consultar estilos calculados, así que el navegador tiene que garantizar que el CSSOM está completo antes de ejecutarlo. El resultado es una cadena en la que una hoja de estilos lenta retrasa la ejecución de un script que la sigue, y ese script retrasa la construcción del resto del DOM.

Localizar dónde se detiene la ruta

Tres señales que se obtienen sin herramientas especiales.

Las diferencias entre marcas de tiempo. Ya vimos que un intervalo grande entre TTFB y FCP indica bloqueo de renderizado. Aquí se puede afinar más:

const nav = performance.getEntriesByType('navigation')[0];
const fcp = performance.getEntriesByName('first-contentful-paint')[0];

console.table({
  ttfb:              Math.round(nav.responseStart),
  finDeDescargaHtml: Math.round(nav.responseEnd),
  domInteractive:    Math.round(nav.domInteractive),
  fcp:               Math.round(fcp?.startTime ?? 0),
  huecoRespuestaAFcp: Math.round((fcp?.startTime ?? 0) - nav.responseEnd),
});

domInteractive marca el momento en que el parser terminó de construir el DOM. Si está muy por encima del final de la descarga del HTML, el parser estuvo detenido: hay scripts síncronos de por medio. Y si el FCP está muy por encima de domInteractive, el DOM estaba listo y faltaba el CSSOM.

Los recursos bloqueantes en la cascada. Filtra por los recursos cuyo tipo iniciador sea link o script y que terminen antes del FCP. Esos son, casi con seguridad, los que están en la ruta crítica.

El orden en el documento. Cualquier hoja de estilos o script síncrono que aparezca en el <head> está en la ruta crítica por construcción. Contarlos en el código fuente es el primer diagnóstico y no cuesta nada.

El objetivo de optimización

Con este modelo, la optimización de la ruta crítica se reduce a tres números que hay que minimizar.

El número de recursos críticos. Cuántos recursos hay que traer antes de poder pintar. Cada uno añade al menos un viaje de ida y vuelta si no está ya en una conexión abierta.

El tamaño total de esos recursos. Cuántos bytes hay que transferir. Aquí entra todo lo del nivel de compresión, y muy especialmente la ventana inicial de la conexión.

La longitud de la cadena. Cuántos recursos críticos no se pueden descubrir hasta que llega otro. Es el peor de los tres, porque encadena viajes en serie en lugar de en paralelo.

Un ejemplo de cómo se aplica. Una página con el HTML, dos hojas de estilo y un script síncrono tiene cuatro recursos críticos. Si una de esas hojas importa otra con una regla de importación, la cadena pasa a tener tres niveles y el tiempo crece aunque los bytes sean los mismos. Fusionar esa importación no reduce ni un byte y quita un viaje entero.

El pecado que más veces he visto en la ruta crítica no es un recurso grande, es una hoja de estilos que importa otras cuatro

Una regla de importación de CSS dentro de otra hoja de estilos es la construcción más cara del rendimiento web por byte de código escrito. El navegador no puede descubrir la hoja importada hasta que ha descargado y parseado la que la importa, así que cada nivel de importación es un viaje de ida y vuelta adicional en serie, y ninguno se solapa con nada. Con cuatro importaciones anidadas y un RTT de 150 milisegundos, son 600 milisegundos de retraso del primer pintado sin que ningún recurso individual sea grande ni ninguna barra de la cascada llame la atención. Lo insidioso es que la construcción parece limpia y modular en el código fuente, y que el escáner de precarga, que es lo que salva a la web de las cadenas de descubrimiento, no puede hacer nada aquí: no lee dentro del CSS. La regla es tajante y no tiene excepciones útiles en producción: ninguna hoja de estilos importada desde otra hoja de estilos en la ruta crítica. Si necesitas modularidad, resuélvela en tiempo de construcción concatenando; si necesitas cargar CSS adicional, referéncialo desde el HTML, donde el escáner lo ve.