Animación hero y Core Web Vitals: mantén LCP, CLS e INP en verde
Por el equipo de Charmotion · Actualizado el

Un hero animado no tiene por qué afectar los Core Web Vitals si la página nunca espera a la animación. Haz que una imagen póster fija sea el elemento LCP, reserva el tamaño del hero con width y height o aspect-ratio, e inicia el runtime de la animación después de la carga, solo cuando el hero sea visible y el movimiento reducido esté desactivado.
Puntos clave
- Un canvas no es candidato a LCP, así que un hero con Rive, canvas de Lottie o three.js deja el LCP a tu imagen póster o a tu titular. Haz que ese póster sea liviano, visible en el HTML y de alta prioridad.
- Para un video, el LCP usa el tiempo de carga del póster o el primer fotograma, lo que ocurra antes, así que un póster liviano ayuda directamente.
- El layout shift viene de dimensiones que faltan: dale a cada contenedor de img, video y canvas un width y un height o un aspect-ratio, y anima transform, no top ni left.
- El INP empeora cuando el código del runtime se ejecuta durante la carga. Carga el runtime en inactividad o cuando el hero entra en la vista, y páusalo cuando sale.
- Con prefers-reduced-motion, conserva el póster y no cargues el runtime en absoluto.
¿Ya tienes un personaje o logo? Mira ejemplos en pantallas reales de apps: pantallas de splash, onboarding, carga y estado vacío, con la animación que se reproduce en cada una.
¿Una animación hero afecta LCP, CLS o INP?
Puede afectar a los tres, cada uno de forma distinta. Los Core Web Vitals de Google se miden en el percentil 75 de las cargas reales de página, y cada uno tiene un objetivo claro:
| Métrica | Bueno | Cómo la afecta una animación hero | La solución |
|---|---|---|---|
| Largest Contentful Paint (LCP) | 2.5 s o menos (malo por encima de 4.0 s) | Un GIF pesado, un video o una imagen que carga tarde se convierte en el elemento más grande y llega tarde | Una imagen póster liviana como elemento LCP, con fetch priority alta, nunca con carga diferida |
| Cumulative Layout Shift (CLS) | 0.1 o menos | El canvas o el video aparece sin espacio reservado y empuja el titular hacia abajo | Atributos width y height o aspect-ratio; anima transform |
| Interaction to Next Paint (INP) | 200 ms o menos (malo por encima de 500 ms) | El JavaScript del runtime, WebAssembly y la configuración de la escena se ejecutan en el hilo principal mientras el usuario intenta tocar | Inicia el runtime en inactividad o al hacerse visible; páusalo fuera de pantalla |
web.dev define el INP como la latencia de "todas las interacciones de clic, toque y teclado" durante una visita, y señala que el retraso de input puede deberse a "tareas largas en el hilo principal." Un hero que arranca una escena 3D mientras un visitante toca el botón del menú es un ejemplo de manual de cómo llegar ahí.
La buena noticia: nada de esto requiere una página estática. Requiere un orden de eventos. Pinta primero un fotograma fijo, mantén fija su caja y luego deja que empiece el movimiento cuando el navegador tenga tiempo.
¿Cuál debería ser el elemento LCP en un hero animado?
Una imagen fija del primer fotograma o del fotograma de reposo del hero, del mismo tamaño que la animación. web.dev enumera los tipos de elementos que considera el LCP: <img>, <image> dentro de SVG, <video>, elementos con una imagen de fondo CSS url() y elementos de bloque que contienen texto. Un <canvas> no está en esa lista, lo que importa para los heroes con Rive, Lottie (renderer canvas) y three.js: el canvas nunca se convierte en el elemento LCP, así que lo será tu titular o tu póster.
Cómo se mide el tiempo de cada formato:
- Video: el LCP usa “el tiempo de carga de la imagen póster o el tiempo de presentación del primer fotograma en los videos”, lo que ocurra primero. Un póster ligero gana.
- GIF y PNG animado: se usa el tiempo de presentación del primer fotograma, pero el navegador igual tiene que descargar una parte suficiente de un archivo grande para mostrarlo.
- Runtimes de canvas: no cuentan, así que el LCP mide la imagen póster que está debajo del canvas o el bloque de texto más grande.
Luego carga ese póster como cualquier imagen LCP. El consejo de web.dev es directo: “Nunca apliques lazy-load a tu imagen LCP”, haz que el recurso sea “detectable desde el código fuente HTML” y define fetchpriority="high" en él (pero en no más de una o dos imágenes). En una prueba de Google Flights, Fetch Priority bajó el LCP de 2,6 s a 1,9 s. El póster de un <video> no tiene su propio atributo fetchpriority, así que si el póster es tu elemento LCP, precárgalo con prioridad alta:
<link rel="preload" as="image" href="/hero-poster.webp" fetchpriority="high">
<div class="hero-art">
<img class="hero-poster" src="/hero-poster.webp" width="960" height="960"
alt="Hushwave logo" fetchpriority="high">
<canvas class="hero-canvas" width="960" height="960" aria-hidden="true" hidden></canvas>
</div>
Usa una opción u otra para cada imagen: el <img> de arriba ya está en el HTML, así que la línea de preload solo hace falta cuando el póster es el póster de un video o un fondo CSS.
¿Cómo evitas que una animación hero provoque layout shift?
Reserva la caja antes de que cargue nada y mantén la animación dentro de ella. La regla de web.dev es: “Incluye siempre los atributos de tamaño width y height en tus elementos de imagen y video” y, cuando no puedas, reserva el espacio con “aspect-ratio de CSS o algo similar”. Un canvas creado por un script no tiene tamaño intrínseco hasta que el runtime le asigna uno, así que dale el tamaño a su contenedor.
.hero-art {
position: relative;
width: min(100%, 480px);
aspect-ratio: 1 / 1;
}
.hero-art > * {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
Como el póster y el canvas están apilados en la misma caja, cambiar uno por otro no altera el diseño. Tres hábitos más mantienen el CLS en cero:
- Anima transforms. web.dev considera seguras las animaciones de transform (translate, scale, rotate, skew) y advierte que “cambiar las propiedades top y left también provoca layout shifts”. Lo mismo pasa con un logo que crece cambiando su ancho.
- Mantén el rebote dentro de la caja. Una mascota de marca que salta o se aplasta necesita la altura del salto dentro de su contenedor, o se recortará. Dimensiona el artboard para la pose más grande, no para la de reposo.
- Nada de banners tardíos encima del hero. Una barra de cookies o un aviso que empuja el hero hacia abajo después de pintarse también es un layout shift, contenga lo que contenga el hero.
¿Cómo cargas el runtime de animación sin perjudicar el INP?
No lo inicies durante la parte crítica de la carga de la página. web.dev señala que “algo que puede alargar el retraso de entrada de una interacción mientras carga una página es la evaluación de scripts”, y un runtime de animación es mucho script: el runtime de Rive incluye un módulo WebAssembly, un reproductor de Lottie convierte JSON en formas y una escena de three.js compila shaders y sube texturas.
Un patrón que sirve para todos: espera a que el hero esté en pantalla (la API Intersection Observer existe para “decidir si realizar o no tareas o procesos de animación según si el usuario verá el resultado”), luego espera un momento de inactividad y después importa el runtime.
const box = document.querySelector(".hero-art");
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function whenIdle(fn) {
if ("requestIdleCallback" in window) requestIdleCallback(fn, { timeout: 2000 });
else setTimeout(fn, 200);
}
async function startHero() {
const { Rive } = await import("@rive-app/canvas-lite");
const canvas = box.querySelector(".hero-canvas");
const r = new Rive({
src: "/hero.riv",
canvas,
stateMachine: "State Machine 1",
autoplay: true,
onLoad: () => {
r.resizeDrawingSurfaceToCanvas();
canvas.hidden = false;
box.querySelector(".hero-poster").hidden = true;
},
});
}
if (!reduce) {
new IntersectionObserver((entries, observer) => {
if (entries[0].isIntersecting) {
observer.disconnect();
whenIdle(startHero);
}
}).observe(box);
}
Notas sobre los detalles:
requestIdleCallbackno es Baseline (MDN lo marca como “Disponibilidad limitada”), así que el ejemplo recurre asetTimeout. MDN también recomienda untimeoutpara el trabajo que sí debe ejecutarse, por eso está en 2 segundos.stateMachine(en singular) es el parámetro que Rive agregó en el runtime v2.41.0; el pluralstateMachinessigue funcionando, pero está obsoleto.- Si precargas
rive.wasm, la documentación de Rive advierte que la URL del preload y la URL que pasas asetWasmUrl()“deben ser exactamente iguales”, o el archivo se descarga dos veces. Precargar también compite con tu imagen LCP, así que hazlo solo cuando el hero sea el motivo de la visita. - Pausa fuera de pantalla. Sigue observando el hero después de que arranque y llama a
r.pause()cuando salga del viewport y ar.play()cuando vuelva. Las máquinas de estados de Rive también “se asientan” y dejan de trabajar cuando nada cambia. En three.js,renderer.setAnimationLoop(null)detiene el loop de render; vuelve a pasar tu función de loop para reanudarlo. - El video necesita menos de esto. La decodificación ocurre en el pipeline multimedia del navegador, así que un
<video>silenciado cuesta sobre todo ancho de banda, no script. El patrón de lazy-loading de web.dev usapreload="none"más un Intersection Observer que inicia la reproducción cuando el video entra en el viewport.
¿Cuánto pesa cada formato de animación hero?
El peso importa incluso cuando la animación no es el elemento LCP: compite por ancho de banda con todo lo demás de la página y, con datos móviles, es dinero del visitante. Estas son cifras de primera mano de nuestro benchmark de peso de archivos de animación (medido el 25 de octubre de 2026), para un loop en reposo de 4 segundos del mismo personaje, tal como se transfiere:
| Formato | Mascota de marca plana | Logo 3D brillante | Transparente |
|---|---|---|---|
| MP4 (H.264, 1080 px) | 211 KB | 558 KB | No |
| Runtime de Rive (373 KB brotli, una vez por sitio) + .riv | unos 385 KB | unos 1.25 MB | Yes |
| WebM con fondo transparente (VP9 con alfa, 1080 px) | 500 KB | 2.0 MB | Yes |
| MOV HEVC con fondo transparente (1080 px) | 743 KB | 1.1 MB | Sí (Safari) |
| GIF (480 px) | 698 KB | 4.4 MB | Solo de 1 bit |
Qué sacar de esto:
- El GIF es el peor formato para un hero. El propio ejemplo de web.dev comparaba un GIF de 3,7 MB con un MP4 de 551 KB y un WebM de 341 KB del mismo clip.
- Hero opaco sobre un fondo sólido: el MP4 es el archivo más ligero.
- Varios estados o interacción con el puntero: Rive, porque cada estado extra son unos pocos kilobytes dentro de un solo archivo en lugar de otro video. El paquete
canvas-litede Rive es la variante más pequeña; prescinde de texto, layout, audio, scripting y Vector Feathering. - Lottie y three.js: no los hemos medido, y ambos dependen mucho de tu build (qué reproductor, qué módulos de three.js, qué tan grandes son el modelo y las texturas). Revisa el tamaño transferido en el panel de red de tu navegador antes de publicar.
Los sets de personaje de Charmotion incluyen un archivo de Rive con máquina de estados, MP4, GIF, WebM con fondo transparente y MOV, además de un póster fijo de cada animación, que es justo el póster que necesitas para el patrón LCP de arriba. Sus animaciones de logo pueden ser un hero de sitio web que reacciona al puntero, entregado con código web para copiar y pegar. Consulta cómo funciona.
¿Qué debe mostrar el hero cuando el movimiento reducido está activado?
El póster y nada más. MDN describe prefers-reduced-motion como el ajuste que le indica al navegador que el usuario “prefiere una interfaz que elimine, reduzca o reemplace las animaciones basadas en movimiento”. Está disponible en todos los navegadores desde enero de 2020, así que no hay motivo para omitirlo.
El script de arriba ya lo comprueba antes de cargar el runtime, lo que también ahorra la descarga. En un hero de video, omite el atributo autoplay e inicia la reproducción desde el script solo cuando el movimiento reducido esté desactivado. En CSS:
@media (prefers-reduced-motion: reduce) {
.hero-canvas, .hero-video { display: none; }
.hero-poster { display: block; }
}
Dos reglas de accesibilidad se aplican a cualquier loop de hero, sea cual sea el ajuste:
- Los loops de más de cinco segundos que empiezan solos y están junto a otro contenido necesitan una forma de pausarlos, detenerlos u ocultarlos (WCAG 2.2 SC 2.2.2, nivel A). Un hero que se reproduce una vez y queda en reposo evita el problema.
- Nada de destellos más de tres veces en un segundo (SC 2.3.1). Los barridos de luz rápidos y las partículas estroboscópicas en un hero 3D son los culpables habituales.
El patrón completo, incluido cómo cambiar un loop de Rive o de video por una pose fija, está en animación accesible y movimiento reducido.
¿Cómo compruebas que la animación hero no perjudicó las Core Web Vitals?
Prueba la página como la ve un teléfono lento, no como la ve tu portátil.
- Encuentra el elemento LCP. En Chrome DevTools, graba una carga de página en el panel Performance y revisa qué elemento está marcado como LCP. Debería ser tu póster o tu titular, no un fotograma de video ni una imagen tardía.
- Revisa las subpartes del LCP. web.dev divide el LCP en tiempo hasta el primer byte, retraso de carga del recurso, duración de carga del recurso y retraso de renderizado del elemento. Un retraso de carga grande significa que el póster se descubrió tarde; una duración de carga grande significa que pesa demasiado.
- Busca layout shifts. La misma grabación muestra las entradas de layout shift. No debería haber ninguna cuando el canvas o el video reemplazan al póster.
- Toca durante la carga. Con la limitación de CPU activada, abre el menú mientras el hero arranca. Si el menú va lento, el runtime arranca demasiado pronto.
- Activa el movimiento reducido en tu sistema operativo y recarga: el hero debería ser una imagen fija y el runtime no debería aparecer en el panel de red.
- Vigila los datos de campo. Las pruebas de laboratorio orientan; lo que cuenta es el percentil 75 de las visitas reales, así que compara antes y después en tu monitoreo de usuarios reales o en el Chrome UX Report.
Cómo agregar una animación hero sin perjudicar las Core Web Vitals
- Exporta un póster. Guarda el primer fotograma o el de reposo de la animación como un WebP o AVIF pequeño al tamaño en que se muestra el hero.
- Haz que el póster sea el elemento LCP. Colócalo en el HTML como un img con width, height y fetchpriority="high". Nunca le apliques lazy-load.
- Reserva la caja. Dale un aspect-ratio al contenedor del hero y apila dentro el póster y el canvas o el video con posicionamiento absoluto.
- Inicia el runtime tarde. Importa el runtime de Rive, Lottie o three.js solo cuando el hero sea visible y el navegador esté inactivo, y luego oculta el póster en el callback de carga.
- Respeta el movimiento reducido. Si prefers-reduced-motion es reduce, deja el póster y no cargues el runtime ni inicies el video.
- Pausa fuera de pantalla y mide. Pausa la animación cuando salga del viewport y luego revisa LCP, CLS e INP con limitación y en datos de campo.
Preguntas frecuentes
¿Un video hero con reproducción automática perjudica el LCP?
Puede hacerlo. En un video, el LCP usa el tiempo de carga del póster o el tiempo de presentación del primer fotograma, lo que ocurra primero. Una imagen póster ligera mantiene el LCP rápido aunque el archivo de video sea grande. Sin póster, el navegador tiene que descargar suficiente video para mostrar el primer fotograma.
¿Un canvas de Rive o three.js cuenta como elemento LCP?
No. La lista de tipos de elemento LCP de web.dev incluye imágenes, elementos de imagen dentro de SVG, videos, imágenes de fondo CSS y bloques de texto, pero no canvas. En un hero con canvas, el LCP suele ser tu titular o la imagen póster debajo del canvas, así que optimiza esos.
¿Debería precargar el archivo WebAssembly de Rive para un hero?
Solo cuando la animación sea lo principal de la página. Precargar inicia la descarga antes, pero compite con tu imagen LCP. Si lo precargas, la documentación de Rive dice que la URL del preload debe coincidir exactamente con la URL que pasas a setWasmUrl(), o el archivo se descarga dos veces.
Fuentes
- Largest Contentful Paint (LCP), web.dev
- Optimize Largest Contentful Paint, web.dev
- Optimize resource loading with the Fetch Priority API, web.dev
- Optimize Cumulative Layout Shift, web.dev
- Interaction to Next Paint (INP), web.dev
- Optimize Interaction to Next Paint, web.dev
- Intersection Observer API, MDN Web Docs
- Window: requestIdleCallback() method, MDN Web Docs
- Lazy loading video, web.dev
- Replace animated GIFs with video for faster page loads, web.dev
- prefers-reduced-motion, MDN Web Docs
- Web (JS) runtime: Rive parameters, Rive
- Canvas vs WebGL2, Rive
- Preloading WASM, Rive
- Web (JS) runtime: state machine playback, Rive
- WebGLRenderer.setAnimationLoop, three.js docs
- Understanding SC 2.2.2: Pause, Stop, Hide, W3C WAI
- Understanding SC 2.3.1: Three Flashes or Below Threshold, W3C WAI
Guías relacionadas
- Benchmark de peso de archivos de animación: Rive vs GIF vs WebM vs MOV vs MP4 (datos reales)
- Cómo agregar un logo animado al header de tu página web
- Animación accesible: movimiento reducido y WCAG
- Video con fondo transparente en sitios web (incluido Safari)
- Rive vs Lottie: rendimiento y peso del runtime
- Animación de logo y mascota con el scroll: CSS, Rive y video