Skip to content
Limited timeStarter is now $9 a month instead of $20.See plans
charmotion

Hero Animation and Core Web Vitals: Keep LCP, CLS and INP Green

By the Charmotion team · Updated

Cover illustration for the guide: Hero Animation and Core Web Vitals: Keep LCP, CLS and INP Green, with Charmotion demo characters

An animated hero does not have to hurt Core Web Vitals if the page never waits for the animation. Make a still poster image the LCP element, reserve the hero's size with width and height or aspect-ratio, and start the animation runtime after load, only when the hero is visible and reduced motion is off.

Key takeaways

  • A canvas is not an LCP candidate, so a Rive, Lottie canvas or three.js hero leaves LCP to your poster image or headline. Make that poster small, discoverable in the HTML and high priority.
  • For a video, LCP uses the poster load time or the first frame, whichever is earlier, so a light poster helps directly.
  • Layout shift comes from missing dimensions: give every img, video and canvas container a width and height or an aspect-ratio, and animate transform, not top or left.
  • INP suffers when runtime code runs during load. Load the runtime on idle or when the hero scrolls into view, and pause it when it scrolls away.
  • Under prefers-reduced-motion, keep the poster and skip loading the runtime at all.

Already have a character or logo? See examples in real app screens: splash, onboarding, loading and empty-state screens, with the animation that plays in each.

Does a hero animation hurt LCP, CLS or INP?

It can hurt all three, each in a different way. Google's Core Web Vitals are measured at the 75th percentile of real page loads, and each has a clear target:

MetricGoodHow a hero animation hurts itThe fix
Largest Contentful Paint (LCP)2.5 s or less (poor above 4.0 s)A heavy GIF, video or late-loading image becomes the largest element and arrives lateA small poster image as the LCP element, high fetch priority, never lazy-loaded
Cumulative Layout Shift (CLS)0.1 or lessThe canvas or video appears without reserved space and pushes the headline downwidth and height attributes or aspect-ratio; animate transform
Interaction to Next Paint (INP)200 ms or less (poor above 500 ms)Runtime JavaScript, WebAssembly and scene setup run on the main thread while the user tries to tapStart the runtime on idle or on visibility; pause it offscreen

web.dev defines INP as the latency of "all click, tap, and keyboard interactions" during a visit, and notes that input delay can be caused by "long tasks on the main thread." A hero that boots a 3D scene while a visitor taps the menu button is a textbook way to get there.

The good news: none of this needs a static page. It needs an order of events. Paint a still frame first, keep its box fixed, then let the motion start when the browser has time.

What should the LCP element be on an animated hero?

A still image of the hero's first or resting frame, the same size as the animation. web.dev lists the element types LCP considers: <img>, <image> inside SVG, <video>, elements with a CSS url() background image, and block elements containing text. A <canvas> is not on that list, which matters for Rive, Lottie (canvas renderer) and three.js heroes: the canvas never becomes the LCP element, so your headline or poster does.

How each format is timed:

  • Video: LCP uses "the poster image load time or first frame presentation time for videos," whichever is earlier. A light poster wins.
  • GIF and animated PNG: the first frame's presentation time is used, but the browser still has to download enough of a large file to show it.
  • Canvas runtimes: not counted, so the poster image under the canvas, or the largest text block, is what LCP measures.

Then load that poster like any LCP image. web.dev's advice is direct: "Never lazy-load your LCP image," make the resource "discoverable from the HTML source," and set fetchpriority="high" on it (but on no more than one or two images). In a Google Flights test, Fetch Priority moved LCP from 2.6 s to 1.9 s. A <video> poster has no fetchpriority attribute of its own, so if the poster is your LCP element, preload it with high priority instead:

<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>

Use one or the other for a given image: the <img> above is already in the HTML, so the preload line is only needed when the poster is a video poster or a CSS background.

How do you stop an animated hero from causing layout shift?

Reserve the box before anything loads, and keep the animation inside it. web.dev's rule is: "Always include width and height size attributes on your images and video elements," and when you cannot, reserve the space with "CSS aspect-ratio or similar." A canvas created by a script has no intrinsic size until the runtime sets one, so give its container the size instead.

.hero-art {
  position: relative;
  width: min(100%, 480px);
  aspect-ratio: 1 / 1;
}
.hero-art > * {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

Because the poster and the canvas are stacked in the same box, swapping one for the other changes no layout. Three more habits keep CLS at zero:

  • Animate transforms. web.dev lists transform animations (translate, scale, rotate, skew) as safe and warns that "changing the top and left properties also cause layout shifts." The same goes for a logo that grows by changing its width.
  • Keep overshoot inside the box. A mascot that jumps or squashes needs the jump height inside its container, or it will be clipped. Size the artboard for the largest pose, not the resting one.
  • No late banners above the hero. A cookie bar or announcement that pushes the hero down after it paints is a layout shift too, whatever the hero contains.

How do you load the animation runtime without hurting INP?

Do not start it during the critical part of page load. web.dev notes that "one thing that can extend an interaction's input delay while a page loads is script evaluation," and an animation runtime is a lot of script: the Rive runtime includes a WebAssembly module, a Lottie player parses JSON into shapes, and a three.js scene compiles shaders and uploads textures.

A pattern that works for all of them: wait until the hero is on screen (the Intersection Observer API exists for "deciding whether or not to perform tasks or animation processes based on whether or not the user will see the result"), then wait for an idle moment, then import the 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);
}

Notes on the details:

  • requestIdleCallback is not Baseline (MDN marks it "Limited availability"), so the sample falls back to setTimeout. MDN also recommends a timeout for work that must happen, which is why it is set to 2 seconds.
  • stateMachine (singular) is the parameter Rive added in runtime v2.41.0; the plural stateMachines still works but is deprecated.
  • If you preload rive.wasm, Rive's docs warn the preload URL and the URL passed to setWasmUrl() "must be exactly the same," or the file downloads twice. Preloading also competes with your LCP image, so only do it when the hero is the reason for the visit.
  • Pause offscreen. Observe the hero after it starts too, and call r.pause() when it leaves the viewport and r.play() when it returns. Rive state machines also "settle," stopping work when nothing changes. For three.js, renderer.setAnimationLoop(null) stops the render loop; pass your loop function again to resume.
  • Video needs less of this. Decoding runs in the browser's media pipeline, so a muted <video> mostly costs bandwidth, not script. web.dev's lazy-loading pattern uses preload="none" plus an Intersection Observer that starts playback when the video enters the viewport.

How heavy is each hero animation format?

Weight still matters even when the animation is not the LCP element: it competes for bandwidth with everything else on the page, and on mobile data it is the visitor's money. These are first-hand numbers from our animation file size benchmark (measured October 25, 2026), for one 4-second idle loop of the same character, as transferred:

FormatFlat mascotGlossy 3D logoTransparent
MP4 (H.264, 1080 px)211 KB558 KBNo
Rive runtime (373 KB brotli, once per site) + .rivabout 385 KBabout 1.25 MBYes
Transparent WebM (VP9 alpha, 1080 px)500 KB2.0 MBYes
Transparent HEVC MOV (1080 px)743 KB1.1 MBYes (Safari)
GIF (480 px)698 KB4.4 MB1-bit only

What to take from it:

  • GIF is the worst hero format. web.dev's own example was a 3.7 MB GIF against a 551 KB MP4 and a 341 KB WebM of the same clip.
  • Opaque hero on a solid background: MP4 is the lightest file.
  • Several states or pointer interaction: Rive, because every extra state is a few kilobytes inside one file instead of another video. Rive's canvas-lite package is the smallest variant; it drops text, layout, audio, scripting and Vector Feathering.
  • Lottie and three.js: we have not measured these, and both depend heavily on your build (which player, which three.js modules, how large the model and textures are). Check the transferred size in your browser's network panel before shipping.

Charmotion's character sets ship a Rive file with a state machine, MP4, GIF, transparent WebM and MOV, plus a still poster of each animation, which is exactly the poster you need for the LCP pattern above. Its logo animations can be a website hero that reacts to the pointer, delivered with copy-paste web code. See how it works.

What should the hero show when reduced motion is on?

The poster, and nothing else. MDN describes prefers-reduced-motion as the setting that tells the browser the user "prefers an interface that removes, reduces, or replaces motion-based animations." It has been available across browsers since January 2020, so there is no reason to skip it.

The script above already checks it before loading the runtime, which also saves the download. For a video hero, leave out the autoplay attribute and start playback from script only when reduced motion is off. In CSS:

@media (prefers-reduced-motion: reduce) {
  .hero-canvas, .hero-video { display: none; }
  .hero-poster { display: block; }
}

Two accessibility rules apply to any hero loop, whatever the setting:

  • Loops longer than five seconds that start automatically and sit beside other content need a way to pause, stop or hide them (WCAG 2.2 SC 2.2.2, Level A). A hero that plays once and rests avoids the problem.
  • No flashing more than three times in any one second (SC 2.3.1). Fast light sweeps and strobing particles in a 3D hero are the usual culprits.

The full pattern, including how to swap a Rive or video loop for a still pose, is in accessible animation and reduced motion.

How do you check that the hero animation did not hurt Core Web Vitals?

Test the page the way a slow phone sees it, not the way your laptop does.

  1. Find the LCP element. In Chrome DevTools, record a page load in the Performance panel and check which element is marked as LCP. It should be your poster or headline, not a video frame or a late image.
  2. Check LCP's sub-parts. web.dev splits LCP into time to first byte, resource load delay, resource load duration and element render delay. A large load delay means the poster was discovered late; a large load duration means it is too heavy.
  3. Look for layout shifts. The same recording shows layout shift entries. There should be none when the canvas or video replaces the poster.
  4. Tap during load. With CPU throttling on, open the menu while the hero is starting. If the menu lags, the runtime is starting too early.
  5. Turn on reduced motion in your operating system and reload: the hero should be a still image and the runtime should not appear in the network panel.
  6. Watch field data. Lab tests are a guide; the 75th percentile of real visits is what counts, so compare before and after in your real-user monitoring or the Chrome UX Report.

How to add a hero animation without hurting Core Web Vitals

  1. Export a poster. Save the first or resting frame of the animation as a small WebP or AVIF at the hero's display size.
  2. Make the poster the LCP element. Put it in the HTML as an img with width, height and fetchpriority="high". Never lazy-load it.
  3. Reserve the box. Give the hero container an aspect-ratio and stack the poster and the canvas or video inside it with absolute positioning.
  4. Start the runtime late. Import the Rive, Lottie or three.js runtime only when the hero is visible and the browser is idle, then hide the poster in the load callback.
  5. Respect reduced motion. If prefers-reduced-motion is reduce, keep the poster and do not load the runtime or start the video.
  6. Pause offscreen and measure. Pause the animation when it leaves the viewport, then check LCP, CLS and INP with throttling and in field data.

Frequently asked questions

Does an autoplaying hero video hurt LCP?

It can. For a video, LCP uses the poster's load time or the first frame's presentation time, whichever is earlier. A light poster image keeps LCP fast even if the video file is large. Without a poster, the browser has to fetch enough video to show the first frame.

Is a Rive or three.js canvas counted as the LCP element?

No. web.dev's list of LCP element types includes images, image elements inside SVG, videos, CSS background images and text blocks, not canvas. On a canvas hero, LCP is usually your headline or the poster image under the canvas, so optimize those.

Should I preload the Rive WebAssembly file for a hero?

Only when the animation is the main point of the page. Preloading starts the download early but competes with your LCP image. If you do preload it, Rive's docs say the preload URL must match the URL given to setWasmUrl() exactly, or the file downloads twice.

Sources

Related guides