Saltar al contenido

CSS, de a poco

Curso de CSS

Rendimiento

Lección 26 de 37 · 6 min

No todas las propiedades cuestan lo mismo. Cambiar un ancho obliga a recalcular la página; cambiar un transform casi no cuesta nada. Saber cuál es cuál alcanza para que las animaciones no tironeen.

Cómo se dibuja un cuadro

Cada vez que algo cambia, el navegador arma un cuadro nuevo en cuatro pasos:

  1. EstilostyleDecide qué reglas aplican a cada elemento.
  2. LayoutlayoutCalcula el tamaño y la posición de cada caja. Si una caja cambia de tamaño, puede mover a sus vecinas, y a las vecinas de ellas.
  3. PinturapaintRellena píxeles: texto, colores, bordes, sombras.
  4. ComposicióncompositeJunta las capas ya pintadas y las ubica en pantalla.

Un cambio entra en el primer paso que lo afecta y arrastra todos los siguientes. Las propiedades de geometría (width, height, top, margin, padding, font-size) disparan layout. Las de aspecto (color, background-color, box-shadow) solo pintura. transform y opacity se resuelven en la composición.

Qué pasos paga cada cambio en cada cuadro. width paga los cuatro: estilo, layout, pintura y composición. background-color se saltea el layout. transform y opacity solo pagan la composición.

Abrí el panel con cada técnica. Con width, el párrafo se reacomoda en cada cuadro: la línea naranja mide su ancho y el contador dice en cuántos cuadros cambió. Con transform u opacity el párrafo no se entera. La cámara lenta ayuda a ver el texto saltando de línea.

el panel se abre con
el párrafo cambió de ancho en 0 cuadros

Pan de campo

Masa madre · 1 noche

Mezclá la harina con el agua y dejala reposar una hora antes de sumar la masa madre y la sal. Después, cuatro series de pliegues y una noche en la heladera.

  1. estilo: se ejecuta
  2. layout: se ejecuta
  3. pintura: se ejecuta
  4. composición: se ejecuta
ingredientes.css
.ingredientes {
width: 0;
overflow: hidden;
transition: width 350ms cubic-bezier(0.22, 1, 0.36, 1);
}
.ingredientes.abierto {
width: 10rem;
transition-duration: 400ms;
}
@media (prefers-reduced-motion: reduce) {
.ingredientes {
transition-duration: 0s;
}
}
ingredientes.html
<aside data-open class="w-0 overflow-hidden data-open:w-40 transition-[width] ease-[cubic-bezier(0.22,1,0.36,1)] duration-350 data-open:duration-400 motion-reduce:transition-none">
…ingredientes
</aside>

Los pasos de cada propiedad dependen del motor; la tabla de arriba sigue a Chromium. Para verlo en tu página, el panel Performance de las herramientas de desarrollo marca cada cuadro con sus pasos.

Animar transform y opacity

Con una pantalla de 60 cuadros por segundo, cada cuadro tiene unos 16 milisegundos. Una animación que solo toca transform u opacity puede correr en el compositor, fuera del hilo principal: sigue fluida aunque JavaScript esté ocupado. Para casi cualquier movimiento hay un reemplazo:

  1. MovertranslateEn lugar de left o top.
  2. AgrandarscaleEn lugar de width y height.
  3. ApareceropacityEn lugar de visibility o display a secas.
toast.css
/* cada cuadro recalcula el layout */
.toast { transition: top 250ms; }
/* cada cuadro solo compone */
.toast {
transition:
translate 250ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 250ms ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
/* sin movimiento, el fundido queda */
.toast { transition-property: opacity; }
}

translate, scale y rotate son propiedades sueltas que funcionan igual que transform. Son Baseline ampliamente disponible (Chrome 104, Firefox 72, Safari 14.1).

will-change, con cuidado

will-change: transform avisa que un elemento va a animarse, y el navegador lo sube a su propia capa antes de empezar. Así el primer cuadro no tiene que pintarlo de cero. Cada capa ocupa memoria de video: si se lo ponés a cien elementos, o a *, la página se pone más lenta, no más rápida.

  1. Usalo en lo que se anima seguido, o agregalo justo antes (al pasar el puntero por el padre) y sacalo al terminar.
  2. Tiene efectos visibles: con transform u opacity crea un contexto de apilamiento, igual que si la propiedad ya estuviera puesta.
  3. No lo agregues por las dudas. Si una animación anda bien sin él, no hace falta.
galeria.css
.galeria:hover .foto {
will-change: transform; /* se prepara mientras el puntero está cerca */
}

will-change es Baseline ampliamente disponible (Chrome 36, Firefox 36, Safari 9.1).

contain

contain le promete al navegador que un elemento está aislado del resto. Con esa promesa, un cambio adentro no obliga a recalcular la página entera. Cada valor es una promesa distinta:

  1. layoutLo de adentro no afecta el layout de afuera. El elemento pasa a ser el bloque contenedor de sus hijos con position: absolute o fixed.
  2. paintNada se pinta fuera de su caja; recorta como overflow: hidden, y si queda fuera de pantalla no se pinta.
  3. sizeSu tamaño no depende de los hijos. Sin un tamaño explícito colapsa a 0. inline-size hace lo mismo solo con el ancho: es lo que usan las container queries.
  4. content · strictcontent = layout paint style, el atajo seguro. strict = size layout paint style, solo si le das tamaño.
clima.css
.widget-del-clima {
contain: content; /* lo que pase adentro, queda adentro */
}

contain es Baseline ampliamente disponible (Chrome 52, Firefox 69, Safari 15.4); el valor inline-size llegó después (Chrome 105, Firefox 101, Safari 16) y también es ampliamente disponible.

content-visibility

content-visibility: auto va un paso más allá: el navegador saltea el layout y la pintura de lo que está lejos de la pantalla, y lo dibuja cuando se acerca. El contenido sigue en el DOM: lo encuentra la búsqueda del navegador, lo leen los lectores de pantalla y los enlaces internos llegan.

Mientras está salteado, el bloque no tiene tamaño. contain-intrinsic-size le da uno de reemplazo, para que la barra de scroll no salte. Con auto delante, el navegador recuerda el tamaño real la primera vez que lo dibuja y usa ese.

El horario de abajo tiene un bloque por hora. La tira de la derecha marca en naranja los bloques que el navegador está dibujando. Bajá por la lista y mirá cómo cambian. Después probá none: los bloques salteados miden 0 y el alto del scroll se achica.

.hora { content-visibility: auto }dibujados: 19 de 19 · alto: —px

Salidas desde Retiro

Línea Mitre · días hábiles

05 h

  • 05:00Tigreandén 1
  • 05:12José León Suárezandén 3
  • 05:24Bartolomé Mitreandén 5
  • 05:36Tigreandén 1
  • 05:48José León Suárezandén 3

06 h

  • 06:00Bartolomé Mitreandén 5
  • 06:12Tigreandén 1
  • 06:24José León Suárezandén 3
  • 06:36Bartolomé Mitreandén 5
  • 06:48Tigreandén 1

07 h

  • 07:00José León Suárezandén 3
  • 07:12Bartolomé Mitreandén 5
  • 07:24Tigreandén 1
  • 07:36José León Suárezandén 3
  • 07:48Bartolomé Mitreandén 5

08 h

  • 08:00Tigreandén 1
  • 08:12José León Suárezandén 3
  • 08:24Bartolomé Mitreandén 5
  • 08:36Tigreandén 1
  • 08:48José León Suárezandén 3

09 h

  • 09:00Bartolomé Mitreandén 5
  • 09:12Tigreandén 1
  • 09:24José León Suárezandén 3
  • 09:36Bartolomé Mitreandén 5
  • 09:48Tigreandén 1

10 h

  • 10:00José León Suárezandén 3
  • 10:12Bartolomé Mitreandén 5
  • 10:24Tigreandén 1
  • 10:36José León Suárezandén 3
  • 10:48Bartolomé Mitreandén 5

11 h

  • 11:00Tigreandén 1
  • 11:12José León Suárezandén 3
  • 11:24Bartolomé Mitreandén 5
  • 11:36Tigreandén 1
  • 11:48José León Suárezandén 3

12 h

  • 12:00Bartolomé Mitreandén 5
  • 12:12Tigreandén 1
  • 12:24José León Suárezandén 3
  • 12:36Bartolomé Mitreandén 5
  • 12:48Tigreandén 1

13 h

  • 13:00José León Suárezandén 3
  • 13:12Bartolomé Mitreandén 5
  • 13:24Tigreandén 1
  • 13:36José León Suárezandén 3
  • 13:48Bartolomé Mitreandén 5

14 h

  • 14:00Tigreandén 1
  • 14:12José León Suárezandén 3
  • 14:24Bartolomé Mitreandén 5
  • 14:36Tigreandén 1
  • 14:48José León Suárezandén 3

15 h

  • 15:00Bartolomé Mitreandén 5
  • 15:12Tigreandén 1
  • 15:24José León Suárezandén 3
  • 15:36Bartolomé Mitreandén 5
  • 15:48Tigreandén 1

16 h

  • 16:00José León Suárezandén 3
  • 16:12Bartolomé Mitreandén 5
  • 16:24Tigreandén 1
  • 16:36José León Suárezandén 3
  • 16:48Bartolomé Mitreandén 5

17 h

  • 17:00Tigreandén 1
  • 17:12José León Suárezandén 3
  • 17:24Bartolomé Mitreandén 5
  • 17:36Tigreandén 1
  • 17:48José León Suárezandén 3

18 h

  • 18:00Bartolomé Mitreandén 5
  • 18:12Tigreandén 1
  • 18:24José León Suárezandén 3
  • 18:36Bartolomé Mitreandén 5
  • 18:48Tigreandén 1

19 h

  • 19:00José León Suárezandén 3
  • 19:12Bartolomé Mitreandén 5
  • 19:24Tigreandén 1
  • 19:36José León Suárezandén 3
  • 19:48Bartolomé Mitreandén 5

20 h

  • 20:00Tigreandén 1
  • 20:12José León Suárezandén 3
  • 20:24Bartolomé Mitreandén 5
  • 20:36Tigreandén 1
  • 20:48José León Suárezandén 3

21 h

  • 21:00Bartolomé Mitreandén 5
  • 21:12Tigreandén 1
  • 21:24José León Suárezandén 3
  • 21:36Bartolomé Mitreandén 5
  • 21:48Tigreandén 1

22 h

  • 22:00José León Suárezandén 3
  • 22:12Bartolomé Mitreandén 5
  • 22:24Tigreandén 1
  • 22:36José León Suárezandén 3
  • 22:48Bartolomé Mitreandén 5

23 h

  • 23:00Tigreandén 1
  • 23:12José León Suárezandén 3
  • 23:24Bartolomé Mitreandén 5
  • 23:36Tigreandén 1
  • 23:48José León Suárezandén 3
  1. 05:00
  2. 06:00
  3. 07:00
  4. 08:00
  5. 09:00
  6. 10:00
  7. 11:00
  8. 12:00
  9. 13:00
  10. 14:00
  11. 15:00
  12. 16:00
  13. 17:00
  14. 18:00
  15. 19:00
  16. 20:00
  17. 21:00
  18. 22:00
  19. 23:00
contain-intrinsic-size
horario.css
.hora {
content-visibility: auto;
contain-intrinsic-size: auto 220px;
}
horario.html
<section class="[content-visibility:auto] [contain-intrinsic-size:auto_220px]">…</section>

content-visibility: auto es Baseline recién disponible desde septiembre de 2025 (Chrome 85, Firefox 125, Safari 26). Un navegador sin soporte ignora la regla y dibuja todo, como siempre: se puede usar sin respaldo. contain-intrinsic-size es Baseline ampliamente disponible (Chrome 83, Firefox 107, Safari 17).

Sombras y desenfoques grandes

La pintura cuesta según el área y el radio. Un filter: blur(40px) sobre una foto grande, o un box-shadow de 60px de desenfoque, tienen que recalcular muchos píxeles. Una vez está bien; en cada cuadro de una animación, no. backdrop-filter además se vuelve a pintar cada vez que cambia lo que hay detrás, por ejemplo al hacer scroll.

Para animar una sombra, pintala una sola vez en un pseudo-elemento y animá su opacidad:

tarjeta.css
.tarjeta { position: relative; }
.tarjeta::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 12px 32px oklch(0 0 0 / 18%);
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.tarjeta:hover::after { opacity: 1; }

Layout en cada cuadro

Una animación de CSS que toca geometría recalcula el layout en cada cuadro, y si el elemento está en el flujo, mueve todo lo que tiene alrededor. Pasa igual con @keyframes: animar height de 0 a auto, margin o left tiene el costo del panel de arriba con width.

latido.css
/* layout en cada cuadro, y empuja a los vecinos */
@keyframes crecer-ancho { from { width: 40px; } to { width: 48px; } }
/* solo composición, y no mueve a nadie */
@keyframes crecer { from { scale: 1; } to { scale: 1.2; } }

En JavaScript el problema se llama layout thrashing: escribir un estilo y enseguida leer una medida (offsetHeight, getBoundingClientRect()) obliga al navegador a hacer el layout en el momento. En un bucle, eso pasa una vez por elemento. Primero leé todas las medidas y después escribí todos los estilos.

En Tailwind

Tailwind tiene clases para la transición y para will-change y contain. content-visibility y contain-intrinsic-size no tienen clase: van como propiedades arbitrarias.

CSSTailwind
transition: widthtransition-[width]
transition: transformtransition-transform
translate · scale · rotatetranslate-x-full · scale-110 · rotate-3
will-change: transformwill-change-transform
contain: contentcontain-content
content-visibility: auto[content-visibility:auto]
sin movimiento si el sistema lo pidemotion-reduce:transition-none
horario.html
<section class="[content-visibility:auto]
[contain-intrinsic-size:auto_220px]">
…
</section>
<aside class="translate-x-full transition-transform duration-350
data-open:translate-x-0 data-open:duration-400
motion-reduce:transition-none">
…
</aside>