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:
- Estilo
styleDecide qué reglas aplican a cada elemento. - Layout
layoutCalcula 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. - Pintura
paintRellena píxeles: texto, colores, bordes, sombras. - Composición
compositeJunta 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.
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.
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.
- estilo: se ejecuta
- layout: se ejecuta
- pintura: se ejecuta
- composición: se ejecuta
.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;}}
<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:
- Mover
translateEn lugar deleftotop. - Agrandar
scaleEn lugar dewidthyheight. - Aparecer
opacityEn lugar devisibilityodisplaya secas.
/* 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.
- Usalo en lo que se anima seguido, o agregalo justo antes (al pasar el puntero por el padre) y sacalo al terminar.
- Tiene efectos visibles: con
transformuopacitycrea un contexto de apilamiento, igual que si la propiedad ya estuviera puesta. - No lo agregues por las dudas. Si una animación anda bien sin él, no hace falta.
.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:
layoutLo de adentro no afecta el layout de afuera. El elemento pasa a ser el bloque contenedor de sus hijos conposition: absoluteofixed.paintNada se pinta fuera de su caja; recorta comooverflow: hidden, y si queda fuera de pantalla no se pinta.sizeSu tamaño no depende de los hijos. Sin un tamaño explícito colapsa a 0.inline-sizehace lo mismo solo con el ancho: es lo que usan las container queries.content · strictcontent=layout paint style, el atajo seguro.strict=size layout paint style, solo si le das tamaño.
.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.
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
- 05:00
- 06:00
- 07:00
- 08:00
- 09:00
- 10:00
- 11:00
- 12:00
- 13:00
- 14:00
- 15:00
- 16:00
- 17:00
- 18:00
- 19:00
- 20:00
- 21:00
- 22:00
- 23:00
.hora {content-visibility: auto;contain-intrinsic-size: auto 220px;}
<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 { 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.
/* 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.
| CSS | Tailwind |
|---|---|
| transition: width | transition-[width] |
| transition: transform | transition-transform |
| translate · scale · rotate | translate-x-full · scale-110 · rotate-3 |
| will-change: transform | will-change-transform |
| contain: content | contain-content |
| content-visibility: auto | [content-visibility:auto] |
| sin movimiento si el sistema lo pide | motion-reduce:transition-none |
<section class="[content-visibility:auto][contain-intrinsic-size:auto_220px]">…</section><aside class="translate-x-full transition-transform duration-350data-open:translate-x-0 data-open:duration-400motion-reduce:transition-none">…</aside>
Performance
Lesson 26 of 37 · 6 min
Not every property costs the same. Changing a width makes the browser rework the page; changing a transform costs almost nothing. Knowing which is which is enough to keep animations from stuttering.
How a frame gets drawn
Every time something changes, the browser builds a new frame in four steps:
- Style
styleWorks out which rules apply to each element. - Layout
layoutComputes every box's size and position. A box that changes size can move its neighbours, and theirs. - Paint
paintFills in pixels: text, colours, borders, shadows. - Composite
compositePuts the painted layers together on screen.
A change enters at the first step it affects and drags every step after it. Geometry properties (width, height, top, margin, padding, font-size) trigger layout. Appearance properties (color, background-color, box-shadow) only paint. transform and opacity are handled at the composite step.
Open the panel with each technique. With width, the paragraph re-flows on every frame: the orange line measures its width and the counter says how many frames it changed on. With transform or opacity the paragraph never notices. Slow motion helps you see the words jumping between lines.
Country loaf
Sourdough · overnight
Mix the flour and water and let it rest for an hour before adding the starter and the salt. Then four sets of folds and a night in the fridge.
- style: runs
- layout: runs
- paint: runs
- composite: runs
.ingredients {width: 0;overflow: hidden;transition: width 350ms cubic-bezier(0.22, 1, 0.36, 1);}.ingredients.open {width: 10rem;transition-duration: 400ms;}@media (prefers-reduced-motion: reduce) {.ingredients {transition-duration: 0s;}}
<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">…ingredients</aside>
Which steps a property triggers depends on the engine; the table above follows Chromium. To check your own page, the Performance panel in the developer tools breaks every frame into its steps.
Animate transform and opacity
On a 60 Hz screen each frame gets about 16 milliseconds. An animation that only touches transform or opacity can run on the compositor, off the main thread, so it stays smooth even while JavaScript is busy. Almost every movement has a replacement:
- Moving
translateInstead ofleftortop. - Growing
scaleInstead ofwidthandheight. - Appearing
opacityInstead of a barevisibilityordisplayswitch.
/* recomputes layout on every frame */.toast { transition: top 250ms; }/* only composites on every frame */.toast {transition:translate 250ms cubic-bezier(0.22, 1, 0.36, 1),opacity 250ms ease-in-out;}@media (prefers-reduced-motion: reduce) {/* no movement, the fade stays */.toast { transition-property: opacity; }}
translate, scale and rotate are standalone properties that work like transform. They are Baseline widely available (Chrome 104, Firefox 72, Safari 14.1).
will-change, sparingly
will-change: transform announces that an element is about to animate, and the browser moves it to its own layer ahead of time, so the first frame doesn't paint it from scratch. Every layer takes video memory: put it on a hundred elements, or on *, and the page gets slower, not faster.
- Use it on things that animate often, or add it right before (when the pointer enters the parent) and remove it afterwards.
- It has visible side effects: with
transformoropacityit creates a stacking context, as if the property were already set. - Don't add it just in case. If an animation runs fine without it, it isn't needed.
.gallery:hover .photo {will-change: transform; /* gets ready while the pointer is near */}
will-change is Baseline widely available (Chrome 36, Firefox 36, Safari 9.1).
contain
contain promises the browser that an element is isolated from the rest of the page. With that promise, a change inside doesn't force it to recompute the whole page. Each value is a different promise:
layoutWhat's inside doesn't affect layout outside. The element becomes the containing block for children withposition: absoluteorfixed.paintNothing paints outside its box; it clips likeoverflow: hidden, and when it's off screen it isn't painted.sizeIts size doesn't depend on its children. Without an explicit size it collapses to 0.inline-sizedoes the same for the width only: it's what container queries use.content · strictcontent=layout paint style, the safe shorthand.strict=size layout paint style, only when you give it a size.
.weather-widget {contain: content; /* whatever happens inside stays inside */}
contain is Baseline widely available (Chrome 52, Firefox 69, Safari 15.4); the inline-size value came later (Chrome 105, Firefox 101, Safari 16) and is widely available too.
content-visibility
content-visibility: auto goes one step further: the browser skips layout and paint for whatever is far from the screen, and renders it as it gets close. The content stays in the DOM: find-in-page finds it, screen readers read it and in-page links reach it.
While it's skipped, the block has no size. contain-intrinsic-size gives it a stand-in so the scrollbar doesn't jump. With auto in front, the browser remembers the real size the first time it renders the block and uses that.
The timetable below has one block per hour. The strip on the right marks in orange the blocks the browser is rendering. Scroll through the list and watch it change. Then try none: skipped blocks measure 0 and the scroll height shrinks.
Departures from Retiro
Mitre line · weekdays
05 h
- 05:00Tigreplatform 1
- 05:12José León Suárezplatform 3
- 05:24Bartolomé Mitreplatform 5
- 05:36Tigreplatform 1
- 05:48José León Suárezplatform 3
06 h
- 06:00Bartolomé Mitreplatform 5
- 06:12Tigreplatform 1
- 06:24José León Suárezplatform 3
- 06:36Bartolomé Mitreplatform 5
- 06:48Tigreplatform 1
07 h
- 07:00José León Suárezplatform 3
- 07:12Bartolomé Mitreplatform 5
- 07:24Tigreplatform 1
- 07:36José León Suárezplatform 3
- 07:48Bartolomé Mitreplatform 5
08 h
- 08:00Tigreplatform 1
- 08:12José León Suárezplatform 3
- 08:24Bartolomé Mitreplatform 5
- 08:36Tigreplatform 1
- 08:48José León Suárezplatform 3
09 h
- 09:00Bartolomé Mitreplatform 5
- 09:12Tigreplatform 1
- 09:24José León Suárezplatform 3
- 09:36Bartolomé Mitreplatform 5
- 09:48Tigreplatform 1
10 h
- 10:00José León Suárezplatform 3
- 10:12Bartolomé Mitreplatform 5
- 10:24Tigreplatform 1
- 10:36José León Suárezplatform 3
- 10:48Bartolomé Mitreplatform 5
11 h
- 11:00Tigreplatform 1
- 11:12José León Suárezplatform 3
- 11:24Bartolomé Mitreplatform 5
- 11:36Tigreplatform 1
- 11:48José León Suárezplatform 3
12 h
- 12:00Bartolomé Mitreplatform 5
- 12:12Tigreplatform 1
- 12:24José León Suárezplatform 3
- 12:36Bartolomé Mitreplatform 5
- 12:48Tigreplatform 1
13 h
- 13:00José León Suárezplatform 3
- 13:12Bartolomé Mitreplatform 5
- 13:24Tigreplatform 1
- 13:36José León Suárezplatform 3
- 13:48Bartolomé Mitreplatform 5
14 h
- 14:00Tigreplatform 1
- 14:12José León Suárezplatform 3
- 14:24Bartolomé Mitreplatform 5
- 14:36Tigreplatform 1
- 14:48José León Suárezplatform 3
15 h
- 15:00Bartolomé Mitreplatform 5
- 15:12Tigreplatform 1
- 15:24José León Suárezplatform 3
- 15:36Bartolomé Mitreplatform 5
- 15:48Tigreplatform 1
16 h
- 16:00José León Suárezplatform 3
- 16:12Bartolomé Mitreplatform 5
- 16:24Tigreplatform 1
- 16:36José León Suárezplatform 3
- 16:48Bartolomé Mitreplatform 5
17 h
- 17:00Tigreplatform 1
- 17:12José León Suárezplatform 3
- 17:24Bartolomé Mitreplatform 5
- 17:36Tigreplatform 1
- 17:48José León Suárezplatform 3
18 h
- 18:00Bartolomé Mitreplatform 5
- 18:12Tigreplatform 1
- 18:24José León Suárezplatform 3
- 18:36Bartolomé Mitreplatform 5
- 18:48Tigreplatform 1
19 h
- 19:00José León Suárezplatform 3
- 19:12Bartolomé Mitreplatform 5
- 19:24Tigreplatform 1
- 19:36José León Suárezplatform 3
- 19:48Bartolomé Mitreplatform 5
20 h
- 20:00Tigreplatform 1
- 20:12José León Suárezplatform 3
- 20:24Bartolomé Mitreplatform 5
- 20:36Tigreplatform 1
- 20:48José León Suárezplatform 3
21 h
- 21:00Bartolomé Mitreplatform 5
- 21:12Tigreplatform 1
- 21:24José León Suárezplatform 3
- 21:36Bartolomé Mitreplatform 5
- 21:48Tigreplatform 1
22 h
- 22:00José León Suárezplatform 3
- 22:12Bartolomé Mitreplatform 5
- 22:24Tigreplatform 1
- 22:36José León Suárezplatform 3
- 22:48Bartolomé Mitreplatform 5
23 h
- 23:00Tigreplatform 1
- 23:12José León Suárezplatform 3
- 23:24Bartolomé Mitreplatform 5
- 23:36Tigreplatform 1
- 23:48José León Suárezplatform 3
- 05:00
- 06:00
- 07:00
- 08:00
- 09:00
- 10:00
- 11:00
- 12:00
- 13:00
- 14:00
- 15:00
- 16:00
- 17:00
- 18:00
- 19:00
- 20:00
- 21:00
- 22:00
- 23:00
.hour {content-visibility: auto;contain-intrinsic-size: auto 220px;}
<section class="[content-visibility:auto] [contain-intrinsic-size:auto_220px]">…</section>
content-visibility: auto has been Baseline newly available since September 2025 (Chrome 85, Firefox 125, Safari 26). A browser without it ignores the rule and renders everything, as always, so no fallback is needed. contain-intrinsic-size is Baseline widely available (Chrome 83, Firefox 107, Safari 17).
Large shadows and blurs
Paint cost grows with area and radius. A filter: blur(40px) over a large photo, or a box-shadow with a 60px blur, has to work out a lot of pixels. Once is fine; on every frame of an animation it isn't. backdrop-filter also repaints whenever what's behind it changes, such as while scrolling.
To animate a shadow, paint it once on a pseudo-element and animate its opacity:
.card { position: relative; }.card::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;}.card:hover::after { opacity: 1; }
Layout on every frame
A CSS animation that touches geometry recomputes layout on every frame, and if the element is in the flow, it moves everything around it. @keyframes are no different: animating height, margin or left costs what the width panel above costs.
/* layout on every frame, and it pushes its neighbours */@keyframes grow-width { from { width: 40px; } to { width: 48px; } }/* composite only, and it moves nobody */@keyframes grow { from { scale: 1; } to { scale: 1.2; } }
In JavaScript the problem is called layout thrashing: writing a style and then reading a measurement (offsetHeight, getBoundingClientRect()) forces the browser to run layout right then. In a loop, that happens once per element. Read every measurement first, then write every style.
In Tailwind
Tailwind has utilities for transitions, will-change and contain. content-visibility and contain-intrinsic-size have none: they go in as arbitrary properties.
| CSS | Tailwind |
|---|---|
| transition: width | transition-[width] |
| transition: transform | transition-transform |
| translate · scale · rotate | translate-x-full · scale-110 · rotate-3 |
| will-change: transform | will-change-transform |
| contain: content | contain-content |
| content-visibility: auto | [content-visibility:auto] |
| no movement when the system asks | motion-reduce:transition-none |
<section class="[content-visibility:auto][contain-intrinsic-size:auto_220px]">…</section><aside class="translate-x-full transition-transform duration-350data-open:translate-x-0 data-open:duration-400motion-reduce:transition-none">…</aside>