Inicio / Artículos

Piense en la invalidación: un modelo práctico de cómo los navegadores renderizan las páginas

Aprenda a razonar sobre el renderizado del navegador preguntándose qué invalida cada cambio, desde los problemas de layout y el costo de dibujo hasta la composición, la hidratación y la transmisión en tiempo real.

Editor: REACTAPP.TOP11 min de lectura2533 palabras

Fuentes: The Web Page Rendering Process In Browsers: A Deep Dive

Casi todo desarrollador frontend puede recitar la secuencia de renderizado: el HTML se convierte en DOM, el CSS en CSSOM, y luego vienen los pasos de layout, paint y composite. Esa secuencia es una buena introducción, pero resulta casi inútil cuando una página real tiene problemas de rendimiento, ya que describe un único proceso mientras los navegadores realizan continuamente pasos parciales y repetidos. Esta guía reemplaza esa lista memorizada por un modelo práctico basado en una sola pregunta, para que puedas explicar los problemas de layout, las pinturas lentas, las animaciones poco eficientes y las demoras en la hidratación utilizando el mismo razonamiento, además de confirmar tus suposiciones en DevTools.

El renderizado es continuo, no un evento único

Una página nunca se renderiza una sola vez y se deja así. Los scripts modifican el DOM, los cambios en las clases alteran los estilos, las imágenes y las fuentes web llegan tarde, los usuarios pasan el cursor y desplazan la página, las animaciones se actualizan en cada frame, y los frameworks vuelven a renderizar los componentes tras las actualizaciones de estado. Cada uno de estos eventos obliga al motor de renderizado a responder a la misma pregunta:

¿Qué ya no es válido, y cuánto trabajo hay que volver a hacer?

Tener esa pregunta en mente es más valioso que conocer los nombres de las etapas. Es la razón por la cual dos aplicaciones que parecen idénticas pueden tener un rendimiento muy diferente en el mismo teléfono: una sigue provocando cálculos costosos, mientras que la otra aprovecha en gran medida el trabajo que el navegador puede reutilizar.

Paso uno: el navegador crea modelos internos

Cuando llega el HTML, el analizador lo convierte en el DOM. El DOM no es una imagen de la página; es un árbol de nodos de elementos, atributos y texto, además de las relaciones de padre e hijo entre ellos. Tomemos un pequeño fragmento de marcado:

<main>
  <h1>Browser Rendering</h1>
  <p>Understanding what happens after HTML arrives.</p>
</main>

El analizador lo convierte en un árbol que el motor puede consultar y actualizar:

Document
└── main
    ├── h1
    │   └── "Browser Rendering"
    └── p
        └── "Understanding what happens after HTML arrives."

Las hojas de estilo pasan por su propio proceso de procesamiento. El navegador las analiza, aplica la cascada y la herencia, determina qué reglas se aplican a qué elementos y calcula el valor final de cada propiedad para cada elemento. En este punto aún no se ha dibujado nada; el motor está acumulando información y necesita tanto la estructura como los estilos calculados antes de poder determinar la geometría. Un esquema orientado a dependencias se ve así:

HTML ──→ DOM ───────────┐
                        ├──→ Style + Layout ──→ Paint ──→ Composite
CSS  ──→ Styles ────────┘

Trate esa imagen como un mapa de dependencias y no como una descripción de la implementación. Los motores reales contienen muchas más etapas internas, cachés, estructuras de seguimiento de invalidación, múltiples hilos y atajos específicos de la plataforma. Lo que el diagrama muestra correctamente es qué salidas dependen de qué entradas. Si desea conocer con más detalle cómo se resuelven los valores de estilo antes del diseño, nuestro artículo sobre dónde encaja CSS en la pipeline del navegador ofrece información más profunda.

El diseño es un cálculo de dependencias

Con el DOM y los estilos calculados disponibles, el navegador determina la geometría. El diseño debe resolver cuestiones como:

  • ¿Cuál es el ancho y alto de cada caja?
  • ¿Dónde se coloca cada caja?
  • ¿Dónde se ajusta el texto dentro de un contenedor?
  • ¿Dónde terminan los hijos de un elemento?
  • ¿Cómo afecta el tamaño de un elemento a sus elementos vecinos, empujándolos o tirando de ellos?
  • La última pregunta es lo que hace que el diseño sea costoso. Supongamos que un párrafo se vuelve más alto porque aumentó su tamaño de fuente. Todo lo que está después de él en el flujo podría necesitar desplazarse hacia abajo, y la altura de un contenedor podría cambiar, lo cual puede afectar a sus propios elementos hermanos. Un cambio que parece local puede tener efectos en una gran área del documento. Por eso es útil pensar en el diseño como la resolución de una red de dependencias, y no como simplemente colocar cuadros en un lienzo.

    Los motores de renderizado se esfuerzan por limitar los recálculos al subárbol afectado, pero la cantidad de trabajo que realmente se realiza depende del tipo de cambio y de cómo se relacionan los elementos entre sí. Por ejemplo, el contenido dentro de un contenedor de tamaño fijo generalmente tiene menos efectos secundarios que el contenido dentro de uno de tamaño automático.

    La invalidación es más importante que el pipeline

    Imagínese que un script ejecuta card.style.width = "500px". El navegador ahora sabe que su geometría anterior para esa tarjeta, y posiblemente para los elementos cercanos, podría estar desactualizada. Antes del siguiente frame debe determinar qué etapas volver a ejecutar. De forma simplificada:

    Something changes
          │
          ▼
    What does it invalidate?
          │
          ├── Style
          ├── Layout
          ├── Paint
          └── Composite
    

    La observación clave es que los cambios no recorren toda la longitud del pipeline:

    • Un nuevo ancho modifica la geometría, por lo que se vuelve a calcular el diseño y, generalmente, se vuelve a pintar el área afectada después.
    • Un nuevo color de fondo cambia la apariencia sin mover nada, por lo que se necesita una nueva pintura, pero normalmente no es necesario volver a calcular el diseño.
    • Un nuevo valor de transform a menudo se puede aplicar con mucho menos esfuerzo, pero solo cuando las decisiones del navegador sobre capas y las condiciones circundantes permiten que la actualización se gestione en la etapa de composición.

    Por esta razón, consejos generales como “evite las repinturas” o “use solo la GPU” fallan en la práctica. La pregunta diagnóstica útil es siempre: ¿qué tareas invalidó este cambio en particular? Nuestro artículo sobre cuánto cuesta al navegador cada cambio de CSS enumera las propiedades comunes según las etapas que tienden a desencadenar.

    Cómo JavaScript fuerza la ejecución temprana de tareas de renderizado

    Una forma común de generar un renderizado costoso por accidente es entrelazar escrituras y lecturas de diseño. El bucle a continuación establece un ancho para cada tarjeta y solicita de inmediato el ancho medido de dicha tarjeta:

    for (const card of cards) {
      card.style.width = "500px";
      const width = card.getBoundingClientRect().width;
      console.log(width);
    }
    

    No hay nada malo con getBoundingClientRect() en sí. Es una API normal y útil. El problema radica en el orden de las operaciones. La asignación de estilo deja pendiente el cálculo del layout, y la línea siguiente solicita un valor que solo podrá ser correcto una vez que esa tarea pendiente se haya completado. El navegador no tiene más remedio que ejecutar las operaciones de estilo y layout de forma síncrona, justo allí, antes de devolver el número. En un bucle, se produce lo que se conoce como layout thrashing: escribir, leer, escribir, leer, con un cálculo forzado de layout en cada iteración.

    Reestructurando el código de modo que todas las lecturas se realicen primero y todas las escrituras después, se elimina esta sincronización forzada:

    const widths = cards.map(card =>
      card.getBoundingClientRect().width
    );
    
    for (const card of cards) {
      card.style.width = "500px";
    }
    

    Ahora las mediciones se realizan con respecto a un único diseño ya válido, y las escrituras se dejan para que el navegador las procese todas juntas en su próxima renderización. Tenga en cuenta que las dos versiones no son estrictamente equivalentes: la segunda registra los anchos antes del cambio, por lo que utilice este reordenamiento solo cuando realmente sea necesario. Si una lectura depende genuinamente de una escritura anterior, las técnicas habituales son agrupar las escrituras y programar las lecturas para el siguiente frame, por ejemplo con requestAnimationFrame.

    La lección más importante no es “nunca medir el diseño”. JavaScript y el procesamiento de renderizado comparten el mismo hilo y los mismos datos, por lo que tu código puede crear dependencias que obligan al navegador a realizar de inmediato aquello que de otro modo habría programado y procesado de forma eficiente en lotes. El problema del rendimiento en el diseño también tiene otros desencadenantes sutiles, los cuales exploramos en colecciones DOM en tiempo real y problemas de rendimiento en el diseño.

    El proceso de pintado es un costo separado del diseño

    Una vez que se conocen las geometrías, el navegador aún debe determinar cómo se verá cada región: fondos, bordes, glifos de texto, imágenes, sombras, gradientes, efectos de recorte y otros efectos visuales. Una forma compacta de separar estas dos etapas es:

    • El diseño determina dónde se encuentran los elementos y cuán grandes son.
    • El proceso de pintado determina cómo se ven esos elementos.

    Cambiar el background-color generalmente no requiere una nueva geometría para todo el documento, mientras que cambiar su tamaño o posición sí puede ser necesario. No concluyas, sin embargo, que el proceso de dibujo es gratuito. Dibujar áreas grandes, efectos intensos como sombras difusas extensas, cambiar el contenido con frecuencia y una invalidación excesivamente amplia pueden hacer que el tiempo de dibujo sea lo suficientemente alto como para retrasar la reproducción de un frame.

    La rasterización y la composición generan el frame final

    El proceso de dibujo produce instrucciones para dibujar, no necesariamente los píxeles finales. Esas instrucciones se rasterizan luego en datos de píxeles, que suelen dividirse en tiles y se manejan con la capa gráfica de la plataforma. A continuación, la composición reúne las superficies o capas resultantes en el frame que se muestra en la pantalla.

    Este es el escenario en el que transform y opacity ganan su reputación. Cuando las condiciones son adecuadas, animar estas propiedades permite que un elemento se mueva, se escale o se desvanezca sin que el navegador vuelva a calcular el diseño del documento ni vuelva a dibujar su contenido; simplemente recombina las capas existentes.

    No obstante, el popular refrán “transform se ejecuta en la GPU, por lo que siempre es rápido” oculta los costos reales:

    • Cada capa adicional consume memoria, lo cual es importante en dispositivos móviles de bajo rendimiento.
    • El contenido aún debe ser rasterizado en algún momento, y la rasterización puede ser costosa.
    • El hecho de que un elemento reciba su propia capa depende de las heurísticas del navegador y del contexto de renderizado, por lo que el mismo CSS puede comportarse de manera diferente en situaciones distintas.

    La regla fundamental es más sencilla que cualquier lista de propiedades: un cambio que invalida menos datos hace que el motor tenga menos por recalcular.

    El presupuesto de procesamiento está compartido

    Una pantalla de 60Hz se actualiza cada aproximadamente 16.67 milisegundos, ya que 1000 / 60 es más o menos 16.67. A 120Hz ese intervalo se reduce a unos 8.33 milisegundos.

    Ese intervalo no representa un tiempo de CPU reservado exclusivamente para tu código. Dentro de él, el navegador debe gestionar la ejecución de scripts, el cálculo de estilos, el diseño, la pintura, la rasterización, la composición, el manejo de entradas, las llamadas de red y sus propias tareas internas. Por lo tanto, una sola tarea larga en JavaScript puede retrasar la renderización, incluso cuando el trabajo de renderizado en sí fuera sencillo.

    Esta es también la razón por la cual métricas como FCP (First Contentful Paint) e INP (Interaction to Next Paint) existen por separado. Mostrar el contenido rápidamente y responder con prontitud a las interacciones son objetivos relacionados, pero fallan por razones distintas y deben medirse de forma independiente.

    Dónde se construye la interfaz de usuario determina el costo de renderizado

    Antes de que el navegador renderice cualquier cosa, una decisión arquitectónica ya ha influido en la cantidad de trabajo que tendrá que realizar: ¿dónde se genera la interfaz de usuario inicial?

    Renderizado del lado del cliente

    Con el CSR, el servidor envía una estructura HTML mínima y el código de la aplicación, y el dispositivo realiza la mayor parte del trabajo:

    Server
       │
       ├── HTML shell
       └── JavaScript + data
                │
                ▼
             Browser
                │
                ├── Parse JS
                ├── Execute JS
                ├── Fetch data
                ├── Build UI
                └── Render
    

    Renderizado del lado del servidor

    Con el SSR, la obtención de datos, la lógica de la aplicación y la generación del HTML se realizan en el servidor antes de enviar la respuesta:

    Request
       │
       ▼
    Server
       │
       ├── Fetch data
       ├── Execute application logic
       └── Generate HTML
                │
                ▼
             Browser
                │
                ▼
          Parse + Render
    

    SSR permite que el marcado significativo llegue al navegador más rápidamente, mientras que CSR traslada los cálculos y la renderización al hardware del usuario. En general, ninguno es mejor. La elección adecuada depende de cuán compleja sea la aplicación, qué datos necesite, qué tan bien se puedan cachear las respuestas, qué dispositivos tengan los usuarios y cuánto JavaScript tenga que ejecutarse eventualmente en el navegador.

    Hidratación: parecer listo no significa estarlo

    Muchos frameworks modernos combinan estos dos enfoques a través de la hidratación. El servidor envía HTML que puede mostrarse de inmediato, y el JavaScript del lado del cliente inicia la aplicación e integra los controladores de eventos necesarios para la interacción:

    Server HTML
         │
         ▼
    Browser renders content
         │
         ▼
    JavaScript loads
         │
         ▼
    Application initializes
         │
         ▼
    Hydration
         │
         ▼
    Interactive UI
    

    La consecuencia es fácil de pasar por alto: una página puede parecer lista mucho antes de poder responder a un clic. El proceso de carga implica descargar, analizar, compilar y ejecutar una gran cantidad de JavaScript, inicializar componentes y conectar sus listeners, todo en el hilo principal. Por lo tanto, una aplicación grande puede hacer que el contenido aparezca más rápido, pero al mismo tiempo genera un aumento repentino en la carga del hilo principal justo después, que es cuando los usuarios comienzan a hacer clics.

    Esa tensión explica la dirección actual de las estrategias de renderizado: enviar menos JavaScript al cliente, dividir los paquetes y activar solo las partes de la página que realmente necesitan interactividad en lugar de todo el contenido a la vez.

    Cambios en la transmisión según cuando comienza el trabajo

    El SSR clásico construye toda la respuesta antes de enviarla. La transmisión por lotes envía el HTML en fragmentos a medida que está disponible:

    Server
      │
      ├── HTML shell ──────────→ Browser
      ├── Content ─────────────→ Browser
      ├── More content ────────→ Browser
      └── Remaining response ──→ Browser
    

    El navegador puede analizar y renderizar los primeros fragmentos mientras el servidor sigue generando el resto. La transmisión en flujo no hace que el renderizado sea en general más económico. Lo que cambia es el momento en que se puede comenzar con el trabajo útil, lo cual puede acortar el tiempo desde la navegación hasta que aparece el contenido visible. La hidratación progresiva aplica la misma idea en el cliente, distribuyendo la inicialización a lo largo del tiempo en lugar de activar toda la aplicación de una sola vez.

    Observe el proceso en lugar de memorizarlo

    La forma más rápida de desarrollar intuición es observando directamente al navegador. Abra el panel de Rendimiento en Chrome DevTools, grabe una interacción y examine qué se ejecuta cuando cambia la interfaz de usuario. Luego, cree un diseño forzado intencionadamente escribiendo un ancho y leyendo la geometría de inmediato:

    element.style.width = "600px";
    element.getBoundingClientRect();
    

    Grabe nuevamente con una actualización que solo cambie una transformación:

    element.style.transform = "translateX(100px)";
    

    El objetivo del ejercicio no es etiquetar una propiedad como buena y otra como mala. Para cada grabación, responda a las preguntas en orden:

    • ¿Se volvió a calcular el estilo?
    • ¿Se ejecutó el diseño, y fue forzado por un script?
    • ¿Se volvió a pintar alguna área?
    • ¿Ocurrió la rasterización?
    • ¿Podría actualizarse todo únicamente mediante el compositor?

    Una vez que lea las trazas de esta manera, el panel de Rendimiento deja de ser una herramienta de depuración de último recurso y se convierte en una forma de ver cómo el motor reacciona a cada línea de su código.

    Más allá del diagrama de seis pasos

    Todavía vale la pena conocer el flujo típico descrito en los libros de texto:

    HTML → DOM → CSSOM → Layout → Paint → Composite
    

    El sistema real es más complejo. El motor mantiene sincronizadas en todo momento varias representaciones relacionadas, desde el árbol de elementos y los estilos calculados hasta la geometría de los cuadros, las instrucciones de dibujo, las teselas rasterizadas y las capas compuestas. Un cambio rara vez obliga a reconstruir todas ellas. Determina qué sigue siendo válido, marca lo que ya no es útil y reutiliza todo lo posible.

    Puntos clave

    • Sustituya “¿Qué optimización debo aplicar?” por “¿Qué cambió, qué se invalidó y por qué el navegador tuvo que volver a hacer ese trabajo?”
    • El diseño es un problema de dependencias; los cambios que parecen locales pueden afectar todo el documento.
    • Las lecturas y escrituras entrelazadas obligan a un diseño síncrono; sepárelas o posponga las lecturas al siguiente frame.
  • La pintura y la composición tienen sus propios costos; transform y opacity son económicos solo cuando las capas y las condiciones cooperan.
  • El presupuesto de fotogramas se comparte con los scripts y el manejo de entradas, por lo que las tareas largas afectan negativamente incluso cuando el renderizado es económico.
  • CSR, SSR, hidratación y streaming principalmente mueven el trabajo en espacio y tiempo; júzgalos según cuánto trabajo del hilo principal llega al dispositivo del usuario y cuándo.
  • Lecturas relacionadas