Animaciones CSS que no destrozan el rendimiento: reglas y trampas de transform/opacity

¿Te fue útil?

Todo va bien hasta que despliegas “solo una pequeña animación” y tu página empieza a moverse como si fuese a través de melaza. Lo peor: puede verse fluida en tu portátil y convertirse en una presentación de diapositivas en un teléfono de gama media que realmente usan tus clientes.

La animación CSS amiga del rendimiento no es mística. Es un problema de pipeline. Si entiendes qué dispara layout, paint y composición—y lo verificas con las herramientas adecuadas—puedes enviar movimiento que parezca costoso sin serlo realmente.

El pipeline de rendering que realmente estás pagando

Los navegadores no “animan CSS”. Actualizan un grafo de escena bajo plazos estrictos: ~16,7ms por frame para 60Hz, ~8,3ms para 120Hz. Si fallas el plazo, el usuario ve tirones. Y los usuarios son jueces despiadados: culparán a tu producto, no a su dispositivo.

Para trabajo de rendimiento, reduce todo a tres costes:

  1. Layout (a.k.a. reflow): calcular tamaños y posiciones de elementos. Caro porque los cambios pueden propagarse en cascada.
  2. Paint: rasterizar píxeles (texto, bordes, sombras, imágenes) en bitmaps. Caro porque los píxeles son trabajo.
  3. Composite: ensamblar capas pintadas en el frame final, aplicando transforms, opacity, clipping, etc. A menudo más barato y puede ejecutarse en el hilo del compositor.

Cuando la gente dice “usa transform y opacity,” señalan una verdad pragmática: esas propiedades a menudo pueden animarse en la etapa de composición sin volver a ejecutar layout o paint cada frame.

Lo que “solo compositor” realmente te compra

Si el navegador puede mantener un elemento en su propia capa (o tratarlo como una superficie compositada separada), cambiar transform o opacity se convierte en una multiplicación de matrices y una mezcla alfa. Eso no es gratis, pero sí es predecible. Lo predecible es lo que mantiene tu presupuesto de frame intacto cuando el resto de la página está ocupada haciendo… todo lo demás.

Pero “solo compositor” es una promesa condicional, no una ley física. Aún puedes desencadenar paints (o peor, layouts) si el elemento no está aislado, si intersecta con efectos que requieren repintado, o si pides al navegador algo que no puede aplazar a la composición.

El encuadre SRE: el rendimiento es un SLO

En operaciones de producción no aceptamos “generalmente está bien” para la latencia. El rendimiento del movimiento merece la misma disciplina. Trata el jank como latencia en la cola: un par de frames malos en el percentil 99 pueden dominar cómo se siente la UI. Necesitas:

  • conciencia del presupuesto (16,7ms es tu tiempo límite por solicitud)
  • perfilado (tus trazas)
  • detección de regresiones (tus alertas)
  • guardarraíles (tus límites de tasa)

Y sí, puedes degradar el rendimiento de la animación sin tocar la animación. Añades una sombra. Cambias una fuente. Despliegas un nuevo encabezado sticky. Felicidades, tu animación compositada ahora está atascada detrás del trabajo de paint.

La regla transform/opacity (y lo que realmente significa)

La regla es simple: anima transform y opacity cuando te importe la suavidad. La razón es menos simple: esas propiedades pueden aplicarse en tiempo de composición usando texturas previamente pintadas, evitando layout y paint por frame.

Buenas animaciones: cambiar cómo se dibuja algo, no qué es

Usa transform para movimiento y escalado, y opacity para desvanecimientos. En lugar de animar top o left, anima transform: translate(). En lugar de animar width, anima transform: scaleX() en un pseudo-elemento o contenedor interno.

Malas animaciones: cambiar geometría, flujo o efectos costosos de paint

Evita animar propiedades que fuerzan layout:

  • width, height
  • top, left, right, bottom (cuando afectan el layout)
  • margin, padding
  • font-size, line-height (especialmente problemático para texto)

Evita animar propiedades que forzan paints costosos:

  • box-shadow (un radio de desenfoque grande es un impuesto de paint)
  • filter (a veces se compone, a veces es brutal; depende del navegador y del contexto)
  • background-position (puede ser costoso en paint)
  • border-radius (a menudo desencadena repaints; puede ser sorprendentemente caro a escala)

“Pero necesito animar height” — alternativas maduras

A veces realmente necesitas un panel expansible. Aún tienes opciones que no incendian la CPU:

  • Usa transforms en un wrapper interno: mantiene el layout estable y anima un elemento interno recortado con transform: scaleY(). Combínalo con transform-origin: top.
  • Usa max-height solo para rangos pequeños: sigue desencadenando layout, pero el daño puede estar contenido si el subárbol está aislado y es pequeño.
  • Usa estados discretos + reduced motion: a veces la solución correcta de rendimiento son menos frames.
  • Usa Web Animations API para orquestación, pero mantén las mismas elecciones de propiedad. La API no hace mágicamente más barato el layout.

Elige el easing y la duración adecuados (sí, importa)

Animaciones demasiado cortas parecen fallos; demasiado largas se sienten como lag de UI. Un buen valor por defecto es 150–250ms para microinteracciones y 250–400ms para transiciones mayores. Si animas posición a larga distancia, añade un poco más de tiempo o parecerá teletransporte.

También: no apiles tres curvas que se peleen entre sí. Si tu elemento escala, se mueve y se desvanece, mantén la curva consistente a menos que tengas una razón para no hacerlo.

Broma #1: Animar height en un DOM grande es como “reiniciar la base de datos” en plena hora pico: a veces funciona, y no mereces esa victoria.

Una cita, porque es verdad

Idea parafraseada (Werner Vogels): “Todo falla, todo el tiempo.” Planea para ello—especialmente las regresiones de rendimiento.

Hechos y contexto histórico que conviene conocer

Esto no es trivia por trivia. Cada punto explica por qué existe el consejo “transform/opacity” y por qué a veces falla.

  1. Las primeras animaciones CSS se pintaban como cualquier otro cambio de estilo. El impulso hacia animación impulsada por el compositor aceleró cuando los navegadores adoptaron renderizado multihilo y más agresiva layerización.
  2. Los navegadores móviles forzaron el cambio. Las CPUs de escritorio podían soportar muchas animaciones malas. Los límites térmicos móviles hicieron el jank inevitable a menos que el trabajo se moviera fuera del hilo principal.
  3. Las pantallas de alta tasa de refresco cambiaron la barra. 120Hz hace que una animación mediocre parezca peor porque tienes la mitad de tiempo por frame.
  4. “Aceleración por GPU” no es un interruptor único. La composición puede usar GPU; la rasterización podría seguir siendo CPU; y algunos efectos forzan retrocesos por software según drivers y presión de memoria.
  5. Crear capas tiene un coste. Promover demasiados elementos a capas puede aumentar el uso de memoria y la sobrecarga de composición. La “solución” se convierte en un nuevo problema.
  6. El renderizado de fuentes es un impuesto oculto frecuente. Animaciones que hacen repintar texto—especialmente con cambios en AA subpíxel—pueden hundir el rendimiento y verse visualmente inestables.
  7. Los elementos sticky y fixed complican el pipeline de scroll. Muchos navegadores optimizan el desplazamiento manteniéndolo fuera del hilo principal; ciertos efectos (como backdrops pesados) pueden traerlo de vuelta.
  8. Existen primitivas de contención porque el layout es contagioso. contain y content-visibility se introdujeron para reducir el radio de impacto del trabajo de layout/paint en páginas complejas.
  9. “Reduce motion” se convirtió en una característica real de plataforma por una razón. prefers-reduced-motion no es solo accesibilidad; también es una vía de escape de rendimiento en dispositivos lentos.

Trampas: cuando “acelerado por GPU” se vuelve “GPU-molesto”

1) Transform/opacity son rápidos… hasta que fuerzas paint de todas formas

Puedes animar transform en un elemento que aún repinta porque:

  • el elemento no está en su propia capa y el navegador decide que repintar es más barato que aislarlo
  • tiene descendientes que consumen mucho paint que cambian (p. ej., gradientes animados)
  • lo combinas con efectos que requieren repaint (ciertos filtros, modos de mezcla, sombras grandes)

Regla práctica: si los píxeles dentro del elemento son estables, vence la composición. Si los píxeles cambian, estás pagando costes de paint y transform no te salva.

2) will-change es una herramienta poderosa, no un estilo de vida

will-change: transform le dice al navegador: “voy a animar esto pronto; por favor prepárate.” La preparación a menudo significa promover a una capa y asignar memoria. Eso ayuda para un pequeño número de elementos que sabes que van a animar pronto.

Es dañino cuando lo esparces por todas partes “por si acaso.” Los modos de fallo:

  • mayor uso de memoria GPU (más capas, texturas más grandes)
  • más trabajo de composición (más superficies que mezclar)
  • más presión de caché (las texturas se expulsan y se repintan después)
  • peor rendimiento en dispositivos de gama baja (exactamente donde necesitabas ayuda)

Usa will-change como usas precalentar una caché: cercano al evento, muy acotado, y eliminado cuando no se necesita.

3) Oscilación subpíxel y la queja “¿por qué está borroso?”

Los transforms ocurren en espacio de punto flotante. El texto y las líneas finas pueden acabar en medias píxeles, activando diferencias de antialiasing cuadro a cuadro. Lo verás como destello o desenfoque durante el movimiento.

Las soluciones incluyen:

  • traducir en píxeles enteros cuando sea posible (redondea valores en JS si controlas los transforms)
  • evitar animar directamente capas de texto; anima contenedores con rasterización estable
  • considerar translateZ(0) con cuidado (puede cambiar el comportamiento de rasterización)

4) Las capas grandes son capas caras

Si promueves un elemento a pantalla completa (o una lista casi completa) a su propia capa y lo animas, puedes asignar texturas enormes. Eso puede:

  • aumentar el uso de memoria
  • provocar comportamiento de tiling y repaints parciales
  • disparar expulsión de memoria GPU, que causa tirones en el peor momento

Uno de los momentos más comunes de “¿por qué esto empeoró?” es promover un contenedor de desplazamiento grande porque parecía una buena idea.

5) Animaciones que pelean con el scroll

El scroll es sagrado. Los navegadores trabajan mucho para mantener el desplazamiento suave, a veces ejecutándolo fuera del hilo principal. Si tu animación fuerza trabajo en el hilo principal durante el scroll—layout, paint pesado, o JS sincrónico—el jank de scroll aparece de inmediato.

Ten especial cuidado con:

  • JS impulsado por el scroll que lee layout y escribe estilos en el mismo tick
  • encabezados sticky con sombras/backdrops pesados
  • grandes áreas de backdrop-filter (a menudo costosas)

Broma #2: Tu manejador de scroll no necesita ser “tiempo real.” Es una UI, no un escritorio de trading de alta frecuencia.

Guía de diagnóstico rápido

Esta es la lista “la página está entrecortada, ¿qué hago en los próximos 10 minutos?”. Prioriza los cuellos de botella más comunes y los pasos de desambiguación más rápidos.

Primero: confirma qué tipo de jank es

  1. ¿Ocurre durante el scroll? Si sí, sospecha trabajo en el hilo principal (layout/paint/JS) que bloquea el scroll, o composición costosa.
  2. ¿Ocurre durante una animación específica? Si sí, sospecha thrash de layout, efectos pesados de paint, demasiadas capas, o texturas grandes.
  3. ¿Solo en algunos dispositivos? Si sí, sospecha límites de memoria GPU, diferencias de drivers, altos DPR, o throttling térmico.

Segundo: mide antes de “optimizar”

  1. Graba una traza en DevTools Performance con la animación en ejecución.
  2. Comprueba si los frames se pierden por Main (JS/layout) o Raster/Paint o Compositor.
  3. Activa el parpadeo de paint / bordes de capas para ver qué se repinta y qué se compone.

Tercero: aplica la corrección mínima que elimine el cuello de botella

  • Si domina el layout: deja de animar propiedades de layout; añade contención; elimina lecturas síncronas de layout en JS.
  • Si domina el paint: reduce el área de paint; elimina sombras/filtros costosos; aisla el elemento animado; prerenderiza assets estáticos.
  • Si domina la composición: reduce el número de capas; evita superficies promovidas enormes; elimina will-change innecesario.

Cuarto: valida en un perfil de dispositivo realista

Haz throttling de CPU en DevTools, prueba con alto DPR y prueba con reduced motion. Si la solución solo funciona en tu máquina de desarrollo, no es una solución; es una demo.

Tareas prácticas: comandos, salidas, decisiones

Pediste tareas prácticas con comandos, salidas y la decisión resultante. Aquí hay 12+ que se mapean a flujos reales: depuración local, chequeos en CI y investigaciones de “¿por qué solo se rompe en producción?”.

Task 1: Audit which CSS properties are being animated in your codebase

cr0x@server:~$ rg -n "transition-property|transition:|@keyframes|animation:" ./src
src/components/Card.css:14:transition: box-shadow 200ms ease, transform 200ms ease;
src/components/Drawer.css:22:transition: height 250ms ease;
src/styles/animations.css:5:@keyframes pulse { from { opacity: 0.6 } to { opacity: 1 } }

Qué significa: Tienes al menos una transición de height y una transición de box-shadow—riesgos clásicos de layout/paint.

Decisión: Reemplaza la animación de height por una técnica basada en transform o enciérrala en un subárbol pequeño. Considera eliminar la animación de la sombra o falsificarla con opacity en un elemento de sombra prerenderizado.

Task 2: Find “will-change” usage and check if it’s being abused

cr0x@server:~$ rg -n "will-change" ./src
src/styles/global.css:88:.card { will-change: transform; }
src/components/Modal.css:12:.modal { will-change: transform, opacity; }
src/components/List.css:3:.row { will-change: transform; }

Qué significa: Muchos elementos están pre-promovidos, posiblemente incluidas filas de listas (potencialmente cientos).

Decisión: Elimina will-change de cualquier cosa repetida en listas grandes. Aplícalo solo justo antes de la interacción (hover/focus/open) y quítalo después.

Task 3: Verify if you’re shipping reduced-motion support

cr0x@server:~$ rg -n "prefers-reduced-motion" ./src
src/styles/animations.css:42:@media (prefers-reduced-motion: reduce) { .anim { animation: none !important; transition: none !important; } }

Qué significa: Hay al menos un interruptor de apagado básico.

Decisión: Asegúrate de que cubra las interacciones pesadas (drawers, carousels) y no solo desactive animaciones decorativas mientras mantiene transiciones costosas de layout.

Task 4: Catch layout-thrashing patterns in JS (read/write interleaving)

cr0x@server:~$ rg -n "offsetHeight|getBoundingClientRect\\(|scrollTop|clientWidth" ./src
src/pages/Home.ts:91:const h = panel.offsetHeight;
src/pages/Home.ts:92:panel.style.height = (h + 20) + "px";
src/hooks/useSticky.ts:44:const r = el.getBoundingClientRect();

Qué significa: Hay al menos un candidato de lectura-después-de-escritura que puede forzar layout síncrono cada frame si se usa en un bucle o en un manejador de scroll.

Decisión: Agrupa lecturas y escrituras (lee todo el layout primero, luego escribe estilos), o muévete a un enfoque basado en transform que evite leer layout en el camino caliente.

Task 5: Produce a Lighthouse JSON artifact in CI (baseline performance budget)

cr0x@server:~$ lighthouse http://localhost:4173 --output=json --output-path=./artifacts/lh.json --quiet
...Auditing: Performance...
...Report is done...
...Saved JSON report to ./artifacts/lh.json...

Qué significa: Tienes un artefacto reproducible para comparar entre commits. Lighthouse no “calificará tu animación”, pero detectará hinchazón del hilo principal y grandes costes de paint que correlacionan con jank.

Decisión: Añade umbrales (p. ej., tiempo total bloqueado, trabajo del hilo principal) como guardarraíles; cuando se regresen, la suavidad de las animaciones suele degradarse también.

Task 6: Extract long tasks from the Lighthouse artifact (spot main-thread starvation)

cr0x@server:~$ jq '.audits["long-tasks"].details.items[:5]' ./artifacts/lh.json
[
  {
    "startTime": 1234.56,
    "duration": 245.12,
    "url": "http://localhost:4173/assets/app.js",
    "attributableToMainThread": true
  }
]

Qué significa: Las tareas largas por encima de ~50ms matan frames; bloquean la entrada y las animaciones.

Decisión: Divide el trabajo (code splitting), difiere JS no crítico y evita cálculos pesados durante transiciones/scroll.

Task 7: Capture a CPU profile while reproducing the jank (Node tooling for dev servers)

cr0x@server:~$ node --cpu-prof --cpu-prof-dir=./profiles ./node_modules/.bin/vite dev
VITE v5.0.0  ready in 312 ms
  ➜  Local:   http://localhost:5173/
...CPU profile written to ./profiles/CPU.2025-12-29T10-22-11.123Z.cpuprofile...

Qué significa: Si tu servidor de desarrollo está saturando la CPU (tormentas de hot reload, transformaciones pesadas en pasos de build), podrías estar confundiendo jank de tooling con jank de la app.

Decisión: Si el servidor de dev es el cuello de botella, prueba en una build de producción. No optimices CSS basándote en un artefacto en modo dev.

Task 8: Run a production build and serve it locally (remove dev-mode noise)

cr0x@server:~$ npm run build
> build
...dist/assets/index-abc123.js  312.45 kB...
cr0x@server:~$ npx serve -s dist -l 4173
Serving!
Local: http://localhost:4173

Qué significa: Ahora pruebas lo que obtienen los usuarios: JS minificado, CSS optimizado, comportamiento real del bundling.

Decisión: Revisa el jank de animación en modo producción antes de hacer cambios. Si el problema desaparece, era tooling o source maps, no CSS.

Task 9: Use Playwright to produce a deterministic trace during an animation

cr0x@server:~$ npx playwright test --trace on --project chromium
Running 1 test using 1 worker
  ✓ ui-animations.spec.ts:12:1 drawer open should be smooth (4.2s)
Trace file: test-results/ui-animations-drawer/trace.zip

Qué significa: Puedes reproducir lo que pasó y correlacionarlo con la actividad JS y el timing. Es lo más cercano a “tests de regresión de perf” que no arruinan tu vida.

Decisión: Si un commit cambia las trazas significativamente (más tareas largas durante la animación), bloquea el merge o arregla la regresión antes de que llegue a usuarios.

Task 10: Check for giant images that turn fades into bandwidth-to-paint pipelines

cr0x@server:~$ find ./dist -type f -name "*.png" -o -name "*.jpg" -o -name "*.webp" | xargs -I{} sh -c 'printf "%8s  %s\n" "$(stat -c%s "{}")" "{}"' | sort -nr | head
 5242880  ./dist/assets/hero-background.jpg
 1310720  ./dist/assets/product-shot.png
  786432  ./dist/assets/logo.png

Qué significa: Los assets grandes aumentan costes de decodificación y raster. Desvanecer un hero de 5MB puede seguir provocando tirones si la decodificación/rasterización de la imagen ocurre durante la transición.

Decisión: Redimensiona/comprime assets; precarga imágenes críticas; evita animar superficies enormes recién decodificadas que entran en vista.

Task 11: Inspect layer count proxies by finding mass-applied transforms

cr0x@server:~$ rg -n "transform:" ./src | head -n 10
src/components/Row.css:7:transform: translateZ(0);
src/components/Card.css:22:transform: translateY(-2px);
src/components/Toast.css:18:transform: translateX(0);
src/components/Toast.css:22:transform: translateX(120%);

Qué significa: translateZ(0) se usa a menudo como truco para “forzar capa”. Usado ampliamente, puede inflar capas y memoria.

Decisión: Elimina translateZ(0) generalizado. Añade promoción de capa solo donde el perfilado muestre una ganancia.

Task 12: Spot expensive CSS effects that often repaint (shadows, filters, backdrops)

cr0x@server:~$ rg -n "box-shadow:|filter:|backdrop-filter:" ./src
src/styles/global.css:55:box-shadow: 0 20px 60px rgba(0,0,0,0.35);
src/components/Header.css:19:backdrop-filter: blur(12px);
src/components/Avatar.css:9:filter: drop-shadow(0 6px 10px rgba(0,0,0,0.25));

Qué significa: Estos son ofensores frecuentes de paint, especialmente cuando se combinan con animación o scroll.

Decisión: Reduce el radio de blur, reduce el área afectada, o reemplaza con assets prerenderizados. Para backdrops: limita el blur a regiones pequeñas o ofrece un fallback sin blur para dispositivos de gama baja.

Task 13: Quickly detect if you’re animating layout via shorthand transitions

cr0x@server:~$ rg -n "transition:\s*all" ./src
src/components/Button.css:4:transition: all 200ms ease;
src/components/Panel.css:11:transition: all 300ms ease-in-out;

Qué significa: transition: all es una trampa. Puede empezar a animar propiedades que afectan layout accidentalmente cuando alguien cambia el CSS más tarde.

Decisión: Reemplaza por propiedades explícitas (p. ej., transition: transform 200ms ease, opacity 200ms ease) y haz que un linter lo haga cumplir.

Task 14: Add a quick stylelint rule to prevent “transition: all” regressions

cr0x@server:~$ cat .stylelintrc.json
{
  "rules": {
    "declaration-property-value-disallowed-list": {
      "transition": ["/all\\s/"]
    }
  }
}

Qué significa: Esto bloquea la regresión de rendimiento accidental más común en transiciones CSS.

Decisión: Ponlo en CI, falla la build si hay violaciones y fuerza transiciones explícitas para que las características de rendimiento sean estables con el tiempo.

Tres mini-historias corporativas desde las trincheras

Mini-historia 1: El incidente causado por una suposición errónea

Un equipo de producto desplegó un dashboard rediseñado con una elegante animación de “las tarjetas flotan”. La implementación fue disciplinada: solo transform y opacity. Todos se felicitaron porque habían memorizado la regla.

En días, soporte empezó a recibir tickets: “el scroll se congela”, “los botones no responden”, “la página va lenta”. No se reproducía en los dispositivos insignia. Sí se reproducía en teléfonos antiguos y algunos portátiles Windows corporativos con drivers GPU conservadores.

La suposición errónea fue simple: transform/opacity siempre significa solo compositor. En este caso, cada tarjeta contenía un sutil brillo de gradiente animado (un efecto de “skeleton loading”) implementado como animación de background. Esos fondos repintaban. Así que el “transform barato” en 30 tarjetas se convirtió en “repintar 30 tarjetas y luego componerlas” cada frame.

La solución no fue heroica. Eliminaron el brillo una vez llegaron los datos, redujeron el número de tarjetas animando simultáneamente y limitaron el paint con un wrapper que usaba contención. También añadieron un fallback de reduced-motion que deshabilitaba el brillo por completo. El dashboard dejó de entrecortarse y el equipo silenciosamente quitó “GPU-accelerated” de su presentación interna.

Mini-historia 2: La optimización que salió mal

Otra organización tenía un sistema de modales usado en toda la app. Alguien notó que la animación de apertura del modal a veces perdía frames, así que la “optimizó” añadiendo will-change: transform, opacity al modal y overlay, más translateZ(0) a un puñado de componentes “para asegurarse de que se quede en la GPU”.

Funcionó en el caso aislado. También introdujo una fuga lenta de rendimiento en otros lugares. Páginas con listas largas empeoraron después de unas interacciones. La memoria del proceso GPU crecía y no parecía bajar rápidamente. Los usuarios decían “se vuelve más lento cuanto más lo uso”, síntoma inquietantemente preciso.

El problema raíz: promoción de capas por todas partes. Filas de lista con will-change se convirtieron en sus propias superficies. Overlay y modal permanecían promovidos aún cuando no animaban. El compositor tenía más trabajo y más presión de memoria. Bajo presión, las texturas se expulsaban y luego se rasterizaban de nuevo—causando jank durante interacciones no relacionadas.

El rollback fue directo: quitar hacks de translateZ(0), limitar will-change solo al breve intervalo antes de la animación y borrarlo explícitamente al terminar. El rendimiento volvió y la queja de degradación lenta desapareció. La lección quedó: will-change no es un condimento de rendimiento para espolvorear por todas partes.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Un equipo de plataforma mantenía un sistema de diseño usado por decenas de equipos de producto. Ya habían sufrido regresiones por animaciones antes, así que hicieron algo profundamente poco sexy: codificaron primitivas de animación y prohibieron transiciones riesgosas por defecto.

Botones, tarjetas, toasts y drawers usaban mixins compartidos: transforms para movimiento, opacity para desvanecimientos. No transition: all. No animar propiedades de layout en componentes comunes. Si un componente realmente necesitaba animar height, tenía que aislarse detrás de un patrón de wrapper y documentarse.

Entonces llegó un rebranding mayor: nueva tipografía, sombras más pesadas, más blur. La app debería haberse vuelto lenta. En cambio, el daño quedó contenido porque las animaciones centrales no dependían de propiedades costosas de paint. Cuando algunas pantallas sí empeoraron, las trazas fueron legibles: se podía señalar picos de tiempo de paint por nuevos efectos visuales, en lugar de perseguir “bugs fantasma de animación CSS”.

Despacharon a tiempo y la cola de incidentes se mantuvo tranquila. Nadie escribió un post celebratorio porque el único cambio visible fue que nada ardió. Ese es el trabajo.

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: la animación se entrecorta solo cuando empieza

Causa raíz: la promoción de capa y la rasterización ocurren tarde (primer frame de la animación), o una imagen/fuente se decodifica al mismo tiempo.

Solución: pre-promociona brevemente con will-change justo antes de la animación; precarga imágenes críticas; evita swaps de fuentes en el primer uso durante la animación.

2) Síntoma: los efectos hover se sienten “pesados” y retrasan la entrada

Causa raíz: hover activa propiedades costosas de paint (desenfoque de sombra) en muchos elementos de una cuadrícula; la región de repaint es grande.

Solución: anima transform/opacity en su lugar; falsifica sombras mediante opacity en un pseudo-elemento; reduce el radio de blur; reduce el número de ítems que se repintan simultáneamente.

3) Síntoma: paneles expandibles/colapsables pierden frames gravemente

Causa raíz: animar height/max-height provoca recálculo de layout para un subárbol grande cada frame.

Solución: mantén el layout estable y anima un wrapper interno usando transform: scaleY() con clipping; añade contain: layout paint donde sea seguro; evita leer layout en cada tick.

4) Síntoma: aparece jank en el scroll tras añadir un header sticky

Causa raíz: elemento sticky con paint costoso (sombra/blur de backdrop) fuerza trabajo en el hilo principal durante el scroll; el scroll no puede ser totalmente asíncrono.

Solución: simplifica visuales sticky; reduce blur/área del backdrop; considera un header de color sólido en dispositivos de gama baja; verifica con parpadeo de paint.

5) Síntoma: el texto se ve borroso durante la animación

Causa raíz: posicionamiento subpíxel durante transforms; la rasterización cambia al moverse el elemento.

Solución: anima el contenedor en lugar del texto; redondea valores de translate; evita escalar texto; prueba entre navegadores (las heurísticas de rasterización difieren).

6) Síntoma: el rendimiento se degrada con el tiempo, no inmediatamente

Causa raíz: demasiadas capas promovidas (a menudo vía will-change o translateZ(0)) causando presión de memoria y churn de texturas.

Solución: elimina promociones persistentes; mantiene el conteo de capas bajo; limita will-change a animaciones activas solamente.

7) Síntoma: “la animación transform es lenta” en un elemento grande

Causa raíz: el elemento es enorme; componerlo cada frame es caro; pueden ocurrir uploads de texturas o tiling.

Solución: reduce el tamaño de la capa (anima un hijo más pequeño); evita superficies promovidas a pantalla completa; simplifica visuales; prefiere desvanecidos por opacity para regiones grandes.

8) Síntoma: la animación se rompe cuando el contenido cambia durante la transición

Causa raíz: mezclar cambios de layout con animación por transform; cargas de contenido disparan reflow a mitad de la animación.

Solución: bloquea tamaños durante la animación; evita insertar nodos DOM en pleno vuelo; anima placeholders y luego intercambia contenido después.

Listas de verificación / plan paso a paso

Checklist A: diseñar una animación que se mantenga rápida

  1. Define la tarea: ¿es decorativa o funcional? Si es funcional, prioriza la capacidad de respuesta sobre el adorno.
  2. Elige propiedades: por defecto usa transform + opacity. Evita propiedades de layout a menos que el subárbol animado sea diminuto.
  3. Mantén el paint estable: evita animar sombras con mucho blur, filtros y gradientes grandes.
  4. Limita el alcance: anima un contenedor, no docenas de hijos, a menos que lo hayas perfilado.
  5. Elige duraciones: 150–250ms para interacciones pequeñas; 250–400ms para transiciones más grandes. Más corto no siempre es mejor.
  6. Planifica reduced motion: desactiva o simplifica el efecto con prefers-reduced-motion.

Checklist B: desplegar sin regresiones

  1. Prohíbe transition: all en componentes compartidos. Transiciones explícitas mantienen el rendimiento estable.
  2. Linter para patrones riesgosos: will-change masivo, translateZ(0) generalizado, lecturas de layout en manejadores de scroll.
  3. Perfila en un perfil lento: haz throttling de CPU y prueba en hardware de gama media.
  4. Graba trazas antes/después: guárdalas en el PR para evitar debates de “en mi máquina funciona”.
  5. Mide tareas largas: el rendimiento de animaciones suele ser enmascarado por problemas de programación en JS.

Checklist C: arreglar una animación janky existente

  1. Identifica la etapa cara: hilo principal vs paint vs composición.
  2. Si el hilo principal está caliente: elimina animaciones de layout, agrupa lecturas/escrituras DOM, corta tareas largas.
  3. Si el paint está caliente: simplifica visuales, reduce área de repaint, quita efectos con mucho blur, aísla con contención.
  4. Si el compositor está caliente: reduce conteo y tamaño de capas, elimina promociones innecesarias.
  5. Valida de nuevo: misma reproducción, mismo perfil de dispositivo, mismo enfoque de medición.

FAQ

1) ¿Son transform y opacity siempre “gratis” de animar?

No. A menudo son más baratos porque pueden aplicarse en tiempo de composición, pero aún pagas el coste de composición, y podrías seguir disparando paint/layout dependiendo del contexto.

2) ¿Debería usar will-change en todas partes para forzar animaciones suaves?

No. Úsalo con moderación y de forma temporal. El uso excesivo aumenta la presión de memoria y la sobrecarga de composición, lo que puede empeorar el rendimiento—especialmente en dispositivos de gama baja.

3) ¿Sigue siendo buena la triquiñuela translateZ(0)?

Como solución específica: a veces. Como valor por defecto: no. Es una herramienta tosca que puede crear demasiadas capas y degradación de rendimiento a largo plazo.

4) ¿Por qué animar box-shadow se siente tan mal?

Porque sombras borrosas grandes son costosas de paint. Si las animas, a menudo repintas una gran área cada frame. Falsifícalas con opacity en un pseudo-elemento, reduce el blur o evita animarlas.

5) ¿Qué hay de animar filter o backdrop-filter?

A veces se compone, a veces es costoso, y el coste varía por navegador y dispositivo. Trata los filtros como “perfilar primero”. Para backdrops, reduce agresivamente el área afectada.

6) Si solo animo transforms, ¿por qué sigo viendo jank?

Causas comunes: tareas largas en el hilo principal (bloqueando la capacidad del compositor para entregar frames), promoción de capa tardía, fallos de caché de raster, capas enormes, o otras partes de la página repintando durante la animación.

7) ¿La animación CSS siempre es mejor que la animación en JavaScript?

No. CSS es excelente para motion simple y declarativa. JS (o la Web Animations API) es mejor para orquestación, interrupciones y secuenciación. Pero la elección de propiedades importa más que la elección de API.

8) ¿Cómo animo un drawer expansible sin animar height?

Usa un wrapper interno que escales en Y con transform: scaleY() y recorta el overflow. Mantén el layout exterior estable para que el resto de la página no haga reflow cada frame.

9) ¿Por qué se ve suave en escritorio pero no en móvil?

Los móviles tienen presupuestos de CPU y GPU más limitados, mayores ratios de píxel de dispositivo y throttling térmico. Tu “pequeña” área de paint puede volverse enorme en píxeles reales, y la presión de memoria llega antes.

10) ¿Debería desactivar animaciones para todos si el rendimiento es malo?

No castigues a todos los usuarios por un subconjunto de dispositivos. Provee prefers-reduced-motion, simplifica los efectos más pesados y arregla la causa raíz. Desactiva lo puramente decorativo si no aporta valor.

Conclusión: siguientes pasos que puedes hacer

Si quieres animaciones CSS suaves en producción, deja de tratar “transform y opacity” como una superstición y empieza a tratarlas como una hipótesis que verificas.

  1. Audita: elimina transition: all, encuentra propiedades animadas de layout y borra hacks globales de capas.
  2. Perfila: graba trazas e identifica si el cuello de botella es layout, paint o composición.
  3. Arregla mínimamente: cambia a transform/opacity, contiene el layout donde sea seguro y reduce efectos costosos de paint.
  4. Guardarraíl: añade reglas de lint y artefactos en CI para que las regresiones no se desplieguen en un viernes.
  5. Valida en la realidad: perfiles de dispositivos lentos, builds de producción y soporte de reduced motion.

A tus usuarios no les importa que tu animación sea “técnicamente acelerada por GPU.” Les importa que la UI responda de inmediato y que el scroll sea fluido. Construye para eso.

← Anterior
Conceptos básicos del envenenamiento de caché DNS: Endurezca su resolver sin sobreingeniería
Siguiente →
Proxmox CIFS «Permiso denegado»: reparar credenciales, dialecto SMB y opciones de montaje

Deja un comentario