Nuestra web tiene humo WebGL, canvas con órbitas, marquees infinitos, estrellas que titilan y un fondo con grano. Y aun así va fluida. No fue gratis: por el camino pasamos de 60fps a 5 (sí, cinco) y volvimos. Esto es lo que aprendimos, con nombres y apellidos, para que no te pase a ti.
La regla de oro: transform y opacity, nada más
El navegador pinta en capas. Animar transform u opacity solo mueve capas ya pintadas (lo hace la GPU, gratis). Animar casi cualquier otra cosa (filter, box-shadow, width, background-position en áreas grandes) obliga a repintar en cada frame. Un elemento pequeño se perdona; veinte elementos, no.
Las estrellas de las reseñas "respiraban" animando filter: drop-shadow. Cada estrella visible se re-rasterizaba 60 veces por segundo. Con ~25 estrellas en pantalla: stuttering perceptible. Ahora respiran con transform: scale() y el glow es estático. Idéntico aspecto, coste cero.
mix-blend-mode y filter: los asesinos silenciosos
La primera versión de nuestro hero usaba mix-blend-mode para el ruido y filter: blur(90px) para los glows, sobre un canvas que se repinta cada frame. Resultado: 5fps. El blend obliga a recomponer toda la pantalla y el blur re-desenfoca en cada repintado. La cura: glows con radial-gradient pre-difuminados (sin filter) y cero blend modes sobre nada animado. De 5fps a 40+ con el mismo aspecto.
Los bucles rAF que nadie ve
Teníamos una bola de luz que seguía al cursor con interpolación suave. Bonita, y un bucle requestAnimationFrame corriendo siempre, aunque el ratón llevara un minuto quieto. Cada efecto continuo debe responder tres preguntas: ¿se pausa fuera de pantalla? (IntersectionObserver), ¿se pausa con la pestaña oculta? (visibilitychange), ¿se detiene en reposo? Si alguna respuesta es no, está robando batería.
- Canvas del hero: se pausa al salir del viewport y con la pestaña oculta
- Humo WebGL del CTA: igual, más DPR limitado a 1.5 y auto-bajada de resolución si detecta frames lentos
- Vídeos de los megamenús: preload=none y solo reproducen mientras el panel está abierto
backdrop-filter: pagas en cada scroll
Un header con backdrop-filter: blur(16px) re-desenfoca lo que tiene detrás en cada frame de scroll. Bajamos a 12px y aligeramos el fondo: mismo cristal, menos factura. Si tu web "va a tirones al hacer scroll", empieza por aquí.
/* MAL: repinta cada frame */
.star { animation: glow 2s infinite; }
@keyframes glow { 50% { filter: drop-shadow(0 0 9px gold); } }
/* BIEN: compositor puro, mismo efecto */
.star { filter: drop-shadow(0 0 5px gold); /* estático */ }
@keyframes glow { 50% { transform: scale(1.1); } }Cómo lo medimos
Nada de intuición: DevTools → Performance con CPU x4, la capa de Paint flashing encendida (si algo parpadea en verde durante una animación, estás repintando), y grabaciones reales en un portátil modesto. La web debe ir fina en el ordenador de tu cliente, no en el tuyo.
Una animación que baja de 60fps no es un efecto: es un bug con purpurina.
Pablo Huget, Desarrollador web en Intervolutions