Reflujo, repintado, composición: cuánto cuesta al navegador cada cambio en CSS
Acompaña a HTML y CSS a través del DOM, CSSOM, el diseño, el dibujo y la composición, y aprende por qué los cambios de ancho son más costosos que los cambios de color y cómo evitar problemas en el diseño.
Fuentes: Browser Rendering Pipeline Explained: From HTML to Pixels + Reflow vs Repaint
Una sola línea de marcado, como un encabezado, parece algo trivial, pero el navegador realiza una larga secuencia de operaciones antes de que se conviertan en píxeles en la pantalla. Conocer esa secuencia permite responder a preguntas prácticas: por qué redimensionar un elemento puede ralentizar una página, por qué las animaciones suelen escribirse con transform, por qué un cambio puede afectar a docenas de otros elementos, y por qué la manipulación intensiva del DOM genera una interfaz con interrupciones. Esta guía explica cada etapa del pipeline de renderizado y luego utiliza ese modelo para explicar la diferencia entre reflow y repaint, así como los hábitos que mantienen rápidas las imágenes.
<h1>Hello World</h1>
Un vistazo al pipeline
En esencia, el pipeline de renderizado es el proceso que convierte tres tipos de entrada —marcado, estilos y scripts— en una imagen que la pantalla puede mostrar.
HTML + CSS + JavaScript
↓
Pixels
↓
Screen
Desglosada en sus etapas individuales, la secuencia se parece más o menos a esto:
Parse HTML
↓
Build DOM
↓
Parse CSS
↓
Build CSSOM
↓
Create Render Tree
↓
Calculate Styles
↓
Layout / Reflow
↓
Paint / Repaint
↓
Composite
↓
Pixels on Screen
Cuando la gente habla de cuán rápido una página llega a su primer contenido visible, suele denominar a esta secuencia Critical Rendering Path. Tenga en cuenta que se trata de un modelo, no de una especificación para un motor en particular. Chrome, Firefox y Safari implementan las etapas de manera diferente, y en actualizaciones posteriores omiten etapas siempre que pueden demostrar que su ejecución es innecesaria. El modelo sigue siendo lo suficientemente preciso como para analizar el rendimiento, que es lo que importa en el trabajo diario con interfaces frontales.
Etapa 1: análisis del HTML para crear el DOM
Todo comienza con el documento HTML que devuelve el servidor, por ejemplo, una página pequeña con un encabezado y un párrafo:
<!DOCTYPE html>
<html>
<body>
<h1>Hello World</h1>
<p>Welcome to my website.</p>
</body>
</html>
El analizador lee este marcado y genera el Modelo de Objetos de Documento, un árbol de nodos en el que cada elemento y cada fragmento de texto tiene su lugar. Expuesto, el árbol del documento anterior se ve así:
HTML
<html>
│
└── body
│
├── h1
│ └── "Hello World"
│
└── p
└── "Welcome..."
El DOM captura lo que contiene el documento y cómo está anidado. También es la interfaz con la que trabajan tus scripts, por lo que una llamada como la siguiente recorre este árbol para encontrar un nodo:
document.querySelector("h1");
El análisis es incremental. El navegador no espera a que llegue todo el archivo antes de comenzar a crear nodos, y por eso una página larga puede empezar a renderizarse mientras el resto sigue llegando.
Etapa 2: análisis del CSS en el CSSOM
Solo la estructura no dice nada sobre la apariencia. Para eso, el navegador necesita las hojas de estilo, como estas dos reglas simples:
h1 {
color: blue;
font-size: 32px;
}
p {
color: gray;
}
El CSS se analiza para formar una segunda estructura, el CSS Object Model. Una forma útil de separar estos dos árboles es según la pregunta que responde cada uno:
DOM
↓
"What exists on the page?"
CSSOM
↓
"How should those things look?"
El renderizado necesita ambas respuestas. Por esta razón también las hojas de estilo interfieren en el renderizado.
Etapa 3: combinar ambos árboles en el árbol de renderizado
Una vez que están disponibles el DOM y el CSSOM, el navegador los fusiona en un árbol de renderizado.
DOM
+
CSSOM
↓
Render Tree
El árbol de renderizado solo contiene lo que realmente se dibujará, junto con los estilos aplicables a dicho contenido. Tomemos este fragmento:
<div>
<h1>Hello</h1>
<p>World</p>
</div>
Después de la fusión, se puede imaginar como un árbol de cajas visibles que llevan consigo su información de estilo:
Render Tree
div
│
├── h1
│ ├── font-size: 32px
│ └── color: blue
│
└── p
└── color: gray
Todo aquello que no participa en el proceso de renderizado queda excluido. Un elemento con display: none no tiene ninguna entrada en absoluto, lo cual es diferente de visibility: hidden: un elemento oculto sigue ocupando espacio, por lo que permanece en el árbol de estructura y sigue participando en el diseño.
Etapa 4: cálculo de estilos calculados
A continuación, el navegador determina el estilo calculado final para cada elemento que será renderizado. Las hojas de estilo reales contienen reglas superpuestas como estas:
.card {
width: 300px;
}
.card p {
font-size: 16px;
}
Para cada elemento, el motor debe resolver tres cuestiones en orden:
Which rules apply?
↓
Which rule wins?
↓
What is the final computed style?
Responderlas implica aplicar todas las reglas de resolución de CSS:
- la cascada
- la herencia de elementos padre
- la especificidad del selector
- la conversión a valores calculados
- los estilos que dependen de consultas de medios
El resultado es un conjunto resuelto de propiedades por elemento:
DOM + CSSOM
↓
Style Calculation
↓
Computed Styles
Etapa 5: diseño, donde se decide la geometría
En este punto el navegador sabe qué mostrar y cómo debe verse. Lo que aún no sabe es dónde irá cada elemento y cuán grande será.Responder a eso es la función del diseño, que convierte una vista en un conjunto de cajas posicionadas:
Viewport
┌────────────────────────────┐
│ │
│ Header │
│ │
├──────────────┬─────────────┤
│ Sidebar │ Content │
│ │ │
│ │ │
└──────────────┴─────────────┘
Durante el diseño, el motor calcula, para cada caja en el árbol de renderizado:
- ancho y alto
- posición
- márgenes y rellenos
- cómo cada caja se relaciona con sus vecinos y ancestros
- la geometría general de la página
MDN resume esta etapa como la determinación del tamaño y ubicación de cada nodo en el árbol de renderizado. Dado que las cajas dependen unas de otras, el diseño es un problema relacionado con todo el árbol y no con cada elemento por separado.
Reflow: ejecutar nuevamente el diseño
La primera pasada de geometría se denomina simplemente diseño. Cualquier pasada posterior desencadenada por un cambio suele llamarse reflow, y algunos equipos utilizan el término informal relayout para referirse a lo mismo:
Layout
↓
Initial calculation
Reflow / ReLayout
↓
Subsequent layout calculation
El resto de esta guía utiliza reflow, ya que esa es la palabra que encontrarás en la mayoría de las discusiones sobre rendimiento y herramientas.
Qué provoca un reflow
Comencemos con un cuadro que tiene un ancho fijo:
.box {
width: 200px;
}
Ahora, un script cambia ese ancho en tiempo de ejecución:
box.style.width = "500px";
Esa asignación plantea una serie de preguntas que el navegador debe responder antes de poder dibujar el siguiente frame:
The box changed width.
What else is affected?
Does its parent change?
Do neighboring elements move?
Does the text wrap differently?
Does the height change?
Does the entire layout need recalculation?
Por lo tanto, parte o toda la página podría necesitar una nueva pasada de diseño, tras la cual también se ejecutarán las etapas posteriores:
Change width
↓
Style recalculation
↓
Layout / Reflow
↓
Paint
↓
Composite
Propiedades que afectan la geometría
Cualquier propiedad que influya en el tamaño o la posición puede causar problemas de diseño. Los candidatos más comunes son:
width
height
margin
padding
top
left
right
bottom
font-size
Cambiar uno de ellos puede mover o redimensionar al elemento en sí y, potencialmente, a todo lo que está a su alrededor. Por ejemplo, ambas actualizaciones siguientes pueden obligar al navegador a volver a calcular el diseño:
element.style.width = "500px";
element.style.marginLeft = "100px";
font-size debe incluirse en la lista porque los cambios en el tamaño del texto afectan la altura de las líneas y el ajuste automático, lo que a su vez modifica la altura del bloque contenedor y, por ende, la posición de todo lo que sigue después.
Por qué el reflow es costoso
Considere una estructura típica de página con varias regiones y contenido anidado:
Page
│
├── Header
├── Navigation
├── Sidebar
├── Main Content
│ ├── Card
│ ├── Card
│ ├── Card
│ └── Table
└── Footer
Si un elemento cerca de la parte superior crece en altura, cada tarjeta, la tabla y el pie de página que se encuentran debajo pueden necesitar nuevas coordenadas. Un solo cambio geométrico puede afectar a una gran parte de la estructura jerárquica:
One Geometry Change
↓
Layout Recalculation
↓
Many Elements Recalculated
↓
Paint
↓
Composite
Esa expansión hace que el trabajo de diseño innecesario sea costoso, especialmente cuando se repite durante una animación o un arrastre.
Etapa 6: dibujo
Una vez resueltos estos tres aspectos, el navegador puede comenzar a generar píxeles:
What to display
+
Where to display it
+
How it should look
Dibujo convierte los cuadros posicionados en operaciones de dibujo y, finalmente, en píxeles. El contenido dibujado incluye:
- texto
- colores y fondos
- bordes
- sombras
- imágenes
- cualquier otra decoración visible
Layout
↓
Paint
↓
Pixels
MDN describe el dibujo como el paso que convierte los cuadros del diseño en píxeles reales en la pantalla.
Volver a dibujar: nuevos píxeles, misma geometría
Se necesita un volver a dibujar cuando un elemento luce diferente pero ocupa exactamente el mismo espacio. Cambiar el color del texto es el caso típico:
color: red;
Un nuevo color no afecta en nada a ninguno de los siguientes elementos:
Width
Height
Position
Layout relationships
Por lo tanto, el navegador puede omitir por completo el proceso de maquetación y seguir un camino más corto:
Style Recalculation
↓
Paint / Repaint
↓
Composite
Reflow versus repaint lado a lado
Esta distinción es el núcleo de la mayoría de los consejos sobre rendimiento en la renderización, por lo que vale la pena describir con precisión cada uno de los aspectos.
Reflow vuelve a calcular la geometría
Un reflow implica que es necesario volver a calcular el tamaño o la posición de al menos un elemento. Establecer un ancho es el ejemplo típico:
element.style.width = "500px";
Luego se ejecuta toda la cadena de etapas:
Change
↓
Style
↓
Layout / Reflow
↓
Paint
↓
Composite
Repaint solo vuelve a dibujar la apariencia
Un repaint significa que el elemento luce diferente, pero no es necesario volver a calcular su geometría. Establecer un color es suficiente para provocarlo:
element.style.color = "red";
La maquetación queda fuera de esta cadena de procesos:
Change
↓
Style
↓
Paint / Repaint
↓
Composite
Reducidos a una línea cada uno:
RELayout / Reflow
→ Geometry changes
Repaint
→ Visual appearance changes
Un botón que cambia de ancho o color
Imagínese un botón sencillo de llamada a la acción:
┌─────────────────────┐
│ Buy Now │
└─────────────────────┘
Supongamos que su ancho cambia del primer valor al segundo:
width: 200px;
width: 400px;
El botón ahora ocupa el doble de espacio horizontal, lo que puede desplazar a los elementos adyacentes o hacer que la fila se ajuste. Se trata de un cambio geométrico, por lo que el navegador vuelve a organizar el contenido y luego lo vuelve a dibujar:
Width changes
↓
Geometry changes
↓
Reflow
↓
Repaint
Ahora supongamos que solo cambia el color de fondo:
background-color: blue;
El botón mantiene su tamaño y posición exactos, y solo sus píxeles cambian:
Color changes
↓
Geometry unchanged
↓
Repaint
La relación es unidireccional
Cuando cambia el diseño, los elementos terminan en nuevas posiciones, y esas nuevas posiciones requieren nuevos píxeles:
Layout changes
↓
Browser needs new pixels
↓
Paint
En resumen, normalmente un reorganización del contenido va seguida de un nuevo dibujo:
Reflow
↓
Usually followed by
↓
Repaint
Lo contrario no es cierto. Una nueva pintura puede ocurrir por sí sola sin ninguna operación de maquetación:
Repaint
↓
Does NOT necessarily require
↓
Reflow
Etapa 7: composición de capas en un marco
Los motores modernos no siempre pintan toda la página como una única imagen plana. Pueden dividir el contenido en capas separadas y ensamblarlas en un paso final de composición:
Layer 1
+
Layer 2
+
Layer 3
↓
Composite
↓
Final Frame
Por ejemplo, una página puede tener una capa de fondo, una tarjeta en su propia capa y un elemento animado en una tercera:
┌──────────────────────┐
│ Background Layer │
│ │
│ ┌──────────────┐ │
│ │ Card Layer │ │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ Animation │ │
│ └──────────────┘ │
└──────────────────────┘
El compositor apila estas capas para generar cada frame. Algunos elementos pueden ser elevados a su propia capa, y en casos adecuados los navegadores transfieren la composición al GPU. Entonces, un cambio que solo mueve o mezcla una capa existente puede aplicarse sin tener que volver a pintar su contenido.
Por qué transform es el valor predeterminado para los movimientos
Aquí hay un efecto de hover que mueve un cuadro animando el valor left, comenzando desde cero:
.box {
left: 0;
}
y terminando 100 píxeles a la derecha:
.box:hover {
left: 100px;
}
Dado que left forma parte de la posición del elemento en el diseño, cada paso de la transición puede requerir un proceso de rediseño además de una nueva pintura. El mismo movimiento visual puede expresarse mediante una transformación:
.box {
transform: translateX(0);
}
.box:hover {
transform: translateX(100px);
}
Una transformación mueve el elemento ya pintado sin cambiar su cuadro de diseño, por lo que el navegador suele poder manejar toda la animación en el compositor, sin necesidad de realizar rediseños ni pinturas por frame. opacity tiene propiedades similares y es la opción habitual para los efectos de desvanecimiento.
Considérelo como una práctica recomendada fuerte y no como una regla estricta. Si realmente la transformación se procesa en el compositor depende del elemento, de los otros estilos aplicados a él, de cómo se asignan las capas y del motor específico utilizado.
Qué etapas se activan realmente con un cambio
En la mayoría de las actualizaciones de estilo, el proceso se distribuye a lo largo de una de dos ramas después del recálculo del estilo:
Change
↓
Recalculate Style
↓
┌───────┴───────┐
↓ ↓
Layout No Layout
↓ │
Paint │
↓ ↓
└───────┬───────┘
↓
Composite
↓
Screen
Cuanto menos etapas costosas afecte una actualización frecuente, más fácil es mantener una renderización fluida. MDN describe el mismo proceso en etapas: recálculo del estilo, luego disposición, luego pintado y composición donde sea aplicable. Los cambios en transform o opacity en una capa compuesta pueden incluso omitir la etapa de pintado, que es el camino más económico de todos.
El presupuesto de fotogramas: aproximadamente 16.67 ms
La mayoría de las pantallas se actualizan unas 60 veces por segundo, lo que deja un tiempo fijo para cada fotograma:
1 second
÷
60 frames
≈
16.67ms per frame
En otras palabras, para mantener 60 FPS, el navegador dispone de aproximadamente este tiempo por fotograma:
16.67ms
Ese presupuesto de frames es compartido por todo lo que debe ocurrir antes de que se muestre el frame: tu JavaScript, el cálculo de estilos, el diseño, el proceso de dibujo y las tareas internas del navegador. Cuando el total supera ese límite, se descarta el frame y el usuario observa interrupciones en la visualización:
16.67ms
↓
Exceeded
↓
Frame missed
↓
Jank
El efecto es más evidente en interacciones que generan frames de forma continua:
- desplazamiento
- animaciones
- arrastrar y soltar
- cambio de tamaño de ventanas o paneles
- paneles de control que actualizan muchas valores al mismo tiempo
MDN expresa lo mismo: el cálculo de estilos, el diseño y el proceso de dibujo deben caber dentro del presupuesto de frames para que la animación se vea fluida.
Problemas de diseño: lecturas que fuerzan un diseño síncrono
Una forma particularmente común de desperdiciar el presupuesto de frames parece inofensiva en el código. El fragmento a continuación establece un ancho, lee un tamaño, establece una altura y vuelve a leer:
element.style.width = "500px";
console.log(element.offsetWidth);
element.style.height = "500px";
console.log(element.offsetHeight);
El patrón que genera es una alternancia de escrituras y lecturas:
Write
↓
Read layout
↓
Write
↓
Read layout
Normalmente, el navegador pospone el cálculo del layout hasta que está a punto de dibujar el siguiente frame, por lo que varias escrituras de estilo pueden procesarse en una sola pasada. Sin embargo, propiedades como offsetWidth y offsetHeight deben devolver los valores actuales. Leer uno justo después de una escritura obliga al motor a procesar de forma síncrona y inmediata todo el trabajo pendiente relacionado con el estilo y el layout. Si se hace esto repetidamente, especialmente dentro de un bucle sobre muchos elementos, se produce layout thrashing: muchas pasadas completas de layout cuando solo habría sido necesaria una.
La solución es agrupar las operaciones de modo que todas las lecturas se realicen primero y todas las escrituras después:
Bad:
Write
Read
Write
Read
Write
Read
Better:
Read
Read
Read
Write
Write
Write
La forma en que se reestructura el código depende de la aplicación, pero el principio siempre es el mismo: no hacer que el navegador vuelva a calcular el diseño más veces de lo necesario. El blog ofrece un análisis más detallado sobre el problema del cálculo repetido del diseño y las colecciones DOM en tiempo real si desea más ejemplos de recálculos forzados.
Técnicas prácticas para un renderizado más eficiente
Animar transformaciones y opacidad
En lugar de mover un elemento con una propiedad de diseño como esta:
left: 100px;
utilice una transformación que se ajuste al diseño:
transform: translateX(100px);
Para hacer desaparecer y reaparecer un elemento gradualmente, cambie entre estos dos valores:
opacity: 0;
opacity: 1;
generalmente es una mejor opción que animar propiedades que afectan el diseño.
Mantenga los cambios en el DOM al mínimo
Cada mutación del DOM puede programar más tareas para el navegador. Agregar nodos uno tras otro en esta secuencia:
element.appendChild(...);
element.appendChild(...);
element.appendChild(...);
suele ser mejor reemplazarlo construyendo el nuevo contenido fuera del documento (un DocumentFragment, por ejemplo) e insertándolo una sola vez. Cuanto más grande y complejo sea el DOM, mayor será el costo de cada cambio evitable.
Actualizaciones por lotes en lugar de intercalarlas
El código que alterna actualizaciones con tareas de maquetación termina teniendo una secuencia como esta:
Update
↓
Layout
↓
Update
↓
Layout
↓
Update
↓
Layout
El objetivo es proporcionar al navegador un grupo de cambios que pueda procesar juntos:
Update
Update
Update
↓
Browser processes the changes
Para animaciones y otras tareas visuales de alta frecuencia, requestAnimationFrame() programa tus actualizaciones para que se ejecuten justo antes de que el navegador renderice el siguiente frame, lo cual las mantiene alineadas con el ciclo de renderizado en lugar de interferir con él.
No anime las propiedades de layout
Las propiedades que modifican la geometría no son adecuadas para elementos que se ejecutan en cada frame:
width
height
margin
padding
top
left
right
bottom
font-size
Cada una de ellas puede provocar un cambio en el layout. Donde el resultado visual lo permita, anime estas en su lugar:
transform
opacity
Reserve espacio para las imágenes
Una etiqueta de imagen sin dimensiones no indica al navegador cuánto espacio dejar libre:
<img src="image.jpg">
Cuando llega el archivo y se conoce su tamaño real, el contenido circundante se desplaza y el layout se vuelve a calcular. Declarar el tamaño intrínseco de antemano evita esto:
<img
src="image.jpg"
width="800"
height="600"
alt="Example"
/>
Un CSS aspect-ratio logra el mismo efecto en imágenes responsivas. De cualquier manera, el navegador puede reservar el espacio adecuado antes de que se cargue la imagen, lo que evita desplazamientos visibles en el layout.
Mida antes de optimizar
No toda repintura o paso de maquetación constituye un problema que merezca solución; los motores están altamente optimizados y manejan la mayor parte de este trabajo rápidamente. Base su esfuerzo en evidencias mediante un bucle simple:
Performance Problem
↓
Measure
↓
Find Bottleneck
↓
Optimize
↓
Measure Again
El panel de Rendimiento en Chrome DevTools registra una línea de tiempo que muestra:
- Ejecución de JavaScript
- Recálculo de estilos
- Maquetación
- Pintado
- Trabajo de composición
- Tareas prolongadas
- Temporización por frame
Corrija el cuello de botella que realmente revela el perfil, no el código que solo parece costoso al leerlo.
Toda la cadena de procesamiento en una sola imagen
Al unir todo de extremo a extremo, el camino desde el código fuente hasta la pantalla se ve así:
HTML
│
↓
Parse HTML
│
↓
DOM
│
│
CSS ──→ Parse CSS ──> CSSOM
│
↓
DOM + CSSOM
│
↓
Render Tree
│
↓
Style Calculation
│
↓
Layout / Reflow
│
↓
Paint / Repaint
│
↓
Composite
│
↓
Pixels on Screen
Y cuando algo cambia después de la primera renderización, el punto decisivo clave es determinar si la geometría se ve afectada:
CSS / DOM Change
↓
Style Recalculation
↓
Does geometry change?
│
┌──┴──┐
YES NO
│ │
↓ ↓
Layout Paint
│ │
└──┬───┘
↓
Composite
↓
Screen
Puntos clave
Tres resúmenes breves capturan lo más importante. El reflow es la opción costosa:
RELayout / Reflow
↓
Changes geometry
↓
Width / Height / Position / Layout
↓
Usually leads to Paint
La repintado es la más económica:
Repaint
↓
Changes appearance
↓
Color / Background / Shadow / etc.
↓
Doesn't necessarily require Layout
Y en cuanto a las animaciones, la elección de la propiedad determina en gran medida el costo:
Layout properties
↓
Potentially expensive
transform + opacity
↓
Often better for smooth animations
No es necesario conocer los detalles internos de cada motor, pero deberías poder seguir el proceso general:
HTML
↓
DOM
CSS
↓
CSSOM
DOM + CSSOM
↓
Render Tree
↓
Style
↓
Layout
↓
Paint
↓
Composite
↓
Pixels
Teniendo en cuenta ese modelo:
- Los cambios en la geometría, como
width, provocan primero el reflow y luego el repintado; los cambios en la apariencia, comocolor, suelen requerir solo un repintado. transformyopacitya menudo pueden omitir el proceso de layout y el pintado, por lo que son adecuados para las animaciones.- Mezclar escrituras en el DOM con lecturas de layout causa problemas en el layout; es mejor realizar las lecturas antes que las escrituras.
El navegador siempre realizará el trabajo real de convertir el código en píxeles. El objetivo no es eliminar ese trabajo, sino dejar de solicitar más del necesario para la página.
Lecturas relacionadas
- Más allá del tamaño del bundle: encontrar lo que realmente hace lenta su aplicación web — Por qué reducir kilobytes rara vez soluciona un problema de lentitud, y cómo rastrear el tiempo real de espera a través de servidores, procesos en cascada, carga dinámica de recursos, scripts y imágenes de terceros.
- APIs nativas del navegador que reemplazarán a paquetes populares de NPM en 2026 — Explica cómo las características nativas de JavaScript y CSS como Signals, el operador pipeline, Temporal y el posicionamiento por anclaje están reemplazando a paquetes comunes de NPM.
- De dónde proceden los valores de CSS cuando se setea none: Cómo funciona la herencia — Aprenda cómo los navegadores deciden si una propiedad se hereda, por qué los hijos reciben el valor calculado del padre y cómo inherit e initial le brindan un control explícito.
- De 66% a 185px: Cómo resuelven los navegadores los valores CSS antes del diseño — Sigue el camino de un valor CSS a través de las etapas declarado, en cascada, especificado, calculado, utilizado y real, y descubre por qué las unidades relativas y el truco de conversión a rem se comportan de la manera en que lo hacen.
- Estructura, estilo, comportamiento: Cómo dividen el trabajo HTML, CSS y JavaScript — Un mapa fácil de entender para principiantes que muestra qué corresponde a cada uno de HTML, CSS y JavaScript en una página web, cómo colaboran entre sí y qué errores comunes difuminan las líneas que los separan.