7 min read
Lazy Loading Lottie Animations Without Hurting Core Web Vitals
A Lottie animation is just JSON plus a JavaScript player, which means it's invisible to the browser's native image lazy-loading and can silently work against your Core Web Vitals if it's wired up carelessly.
Avoiding layout shift (CLS)
The animation's container has no intrinsic size the way an <img> does until the player mounts and sizes its canvas or SVG. If you don't reserve space up front, the surrounding content jumps the moment the animation initializes.
.lottie-container {
width: 100%;
aspect-ratio: 1 / 1; /* reserve space before the player mounts */
}Don't let it compete with your LCP element
If a large decorative Lottie animation sits above the fold, fetching and parsing its JSON can delay the paint of whatever the browser considers your Largest Contentful Paint element. Two practical fixes: keep hero-area animations small and simple, or load the player library and animation data only after the true LCP content (a heading, a hero image) has already painted.
Only load what's in (or near) the viewport
const observer = new IntersectionObserver(
async (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const res = await fetch('/animations/feature-demo.json');
const data = await res.json();
lottie.loadAnimation({
container: entry.target,
renderer: 'svg',
animationData: data,
loop: true,
autoplay: true,
});
observer.unobserve(entry.target);
}
}
},
{ rootMargin: '200px' } // start fetching slightly before it's visible
);Fetching the JSON only when the container is about to enter the viewport means below-the-fold animations never compete with above-the-fold resources for bandwidth or main-thread time during initial load.
Code-split the player library itself
In a bundler-based app, importing lottie-web or @lottiefiles/dotlottie-web at the top of a component ships it in the main bundle even on pages that never show an animation. A dynamic import() (or, in React, React.lazy / Next.js next/dynamic) keeps the player out of the critical path and loads it alongside the animation data instead.
Keep the JSON itself small
None of the loading strategy above compensates for a bloated animation file. Run it through an optimizer first — a smaller JSON payload means less to fetch, less to parse, and less main-thread work regardless of when you decide to load it.