Stop Using GIFs for UI Demos: Use Optimized Video in Next.js Instead
Learn how to replace heavy GIF animations with optimized videos to dramatically improve your Next.js performance
- Next.js
- Performance
- Frontend
- Web Optimization
When building developer portfolios, SaaS landing pages, or product demos, many developers use GIFs to showcase UI interactions. But GIFs are one of the worst-performing media formats on the web.
In this article, we’ll explore why GIFs hurt performance, why video is a better alternative, and how to implement an optimized video component in Next.js with lazy loading using IntersectionObserver.
The Problem with GIFs
GIFs look simple but they are extremely inefficient.
| Format | Size |
|---|---|
| GIF | 10–20 MB |
| MP4 | 1–2 MB |
| WebM | < 1 MB |
Why GIFs are a bad fit for the web:
- No modern compression
- Large file sizes (often 10–20× heavier than video)
- CPU-heavy decoding
- Cannot stream progressively
- Blocks main thread and hurts page performance
Even Next.js warns about this when using a GIF with the Image component:
import Image from "next/image";
<Image src="/demo.gif" alt="Demo" />;
You’ll see a warning like:
Animated images cannot be optimized.
To bypass the warning you must add unoptimized:
<Image src="/demo.gif" alt="Demo" unoptimized />
That means Next.js serves the full GIF file, which defeats optimization and hurts performance.
The Better Solution: Use Video Instead
Modern sites replace GIFs with looping, muted videos.
Benefits of video over GIF:
- Much smaller file sizes (often 10× or more)
- Hardware-accelerated decoding
- Streaming and progressive loading
- Better performance and Core Web Vitals
- Works well with lazy loading and Intersection Observer
Recommended setup:
- WebM as primary (best compression)
- MP4 as fallback (broad support)
Building an Optimized Video Component
Instead of embedding a raw <video> tag, we can build a reusable optimized component that:
- Lazy-loads video only when it enters the viewport
- Shows a poster image while loading
- Fades in smoothly when the video is ready
- Uses WebM with MP4 fallback
OptimizedVideo.tsx
"use client";
import { useState, useEffect, useRef } from "react";
interface OptimizedVideoProps {
webmSrc: string;
mp4Src: string;
poster: string;
alt: string;
className?: string;
}
const OptimizedVideo = ({
webmSrc,
mp4Src,
poster,
alt,
className = "",
}: OptimizedVideoProps) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setIsInView(true);
}
},
{
threshold: 0.1,
rootMargin: "100px",
},
);
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
useEffect(() => {
if (isInView && videoRef.current) {
videoRef.current.play().catch(() => {});
}
}, [isInView]);
return (
<div ref={containerRef} className="relative w-full h-full">
{!isLoaded && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100 rounded-lg">
<div className="w-10 h-10 border-4 border-gray-300 border-t-gray-700 rounded-full animate-spin" />
</div>
)}
{isInView && (
<video
ref={videoRef}
autoPlay
loop
muted
playsInline
preload="metadata"
poster={poster}
onLoadedData={() => setIsLoaded(true)}
className={`w-full h-full object-cover transition-opacity duration-500 ${
isLoaded ? "opacity-100" : "opacity-0"
} ${className}`}
aria-label={alt}
>
<source src={webmSrc} type="video/webm" />
<source src={mp4Src} type="video/mp4" />
</video>
)}
</div>
);
};
export default OptimizedVideo;
Using the Component
Use the component anywhere you need a demo or hero video:
<OptimizedVideo
webmSrc="/videos/demo.webm"
mp4Src="/videos/demo.mp4"
poster="/videos/demo-poster.jpg"
alt="Product demo"
/>
What you get:
- Video loads only when visible in the viewport
- Poster image appears immediately
- Smooth opacity transition when the video is ready
Optimizing the Video File
The biggest win comes from compressing the source video. Use ffmpeg to generate optimized files.
Create optimized MP4
ffmpeg -i input.mov -vcodec h264 -crf 28 -preset slow -movflags +faststart output.mp4
Create WebM
ffmpeg -i input.mov -c:v libvpx-vp9 -crf 32 -b:v 0 output.webm
Reduce resolution
Most UI demos don’t need full 1080p. Scale down to save more:
ffmpeg -i input.mp4 -vf scale=960:-1 output.mp4
Example size comparison:
| Media | Size |
|---|---|
| GIF | 15 MB |
| MP4 | 1.2 MB |
| WebM | 800 KB |
Final Thoughts
GIFs are convenient but extremely inefficient for the web. For modern apps and landing pages, use looping video instead of GIFs.
With a small optimized video component you get:
- Better performance and Core Web Vitals
- Much smaller file sizes
- Lazy loading and streaming
- Smoother experience for users
If you’re building Next.js portfolios, SaaS landing pages, or UI demos, this approach can noticeably improve page performance.
TL;DR
- GIF — avoid for UI demos; large and unoptimizable.
- Next.js
Imagewith GIF — still not ideal; useunoptimizedonly as a last resort. - Optimized video (WebM + MP4) — preferred: smaller, streamable, and lazy-loadable.