We had a marketing landing page with three animated GIFs above the fold — a hero loop showing the product in action, plus two feature animations. They totaled 11 MB. Lighthouse scored the page at 64 on mobile, with LCP at 4.2s and Total Blocking Time at 380ms. Converting those three GIFs to MP4 — same visual content, same loop behavior — took us to 92 in a single afternoon. This post is the play-by-play.
The starting point
The page was a fairly standard Next.js marketing site. Tailwind, statically generated, deployed to Vercel. The three GIFs were exported from After Effects at 720p, 30fps, with moderate optimization. Sizes:
- Hero animation (5 seconds, 720p): 5.8 MB
- Feature 1 (3 seconds, 600p): 2.9 MB
- Feature 2 (4 seconds, 600p): 2.4 MB
Total animation payload: 11.1 MB. On a Slow 4G connection (Lighthouse mobile profile), that's roughly 27 seconds to fully load. The page rendered text quickly but the hero stayed as a "loading" placeholder for too long, which is exactly what LCP penalizes.
The starting Lighthouse report
| Metric | Score |
|---|---|
| Performance | 64 |
| Largest Contentful Paint | 4.2s |
| Total Blocking Time | 380ms |
| Speed Index | 5.8s |
| Cumulative Layout Shift | 0.04 |
The conversion
We ran each GIF through ffmpeg with two settings: H.264 MP4 for the broad fallback, and VP9 WebM for the smaller payload on browsers that support it. The commands:
ffmpeg -i hero.gif -movflags faststart -pix_fmt yuv420p -crf 22 -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos" -an hero.mp4
ffmpeg -i hero.gif -c:v libvpx-vp9 -crf 30 -b:v 0 -deadline good -cpu-used 2 -row-mt 1 -pix_fmt yuv420p -an hero.webm
The post-conversion sizes were striking:
| Asset | GIF | MP4 (H.264) | WebM (VP9) |
|---|---|---|---|
| Hero | 5.8 MB | 320 KB (-94%) | 240 KB (-96%) |
| Feature 1 | 2.9 MB | 180 KB (-94%) | 140 KB (-95%) |
| Feature 2 | 2.4 MB | 150 KB (-94%) | 110 KB (-95%) |
Total payload dropped from 11.1 MB to 650 KB on H.264, or 490 KB on VP9. That's a 17–22x reduction.
The markup change
The original markup was <img src="hero.gif" alt="...">. The replacement:
<video autoplay loop muted playsinline poster="hero-poster.jpg">
<source src="hero.webm" type="video/webm">
<source src="hero.mp4" type="video/mp4">
</video>
The poster is a still frame (JPEG, ~30 KB) that displays before the video starts decoding. This was the unlock for LCP — the browser counted the poster as the contentful paint, and we hit it in under a second.
The post-conversion Lighthouse report
| Metric | Before | After |
|---|---|---|
| Performance | 64 | 92 |
| LCP | 4.2s | 1.4s |
| TBT | 380ms | 110ms |
| Speed Index | 5.8s | 1.9s |
| CLS | 0.04 | 0.02 |
LCP dropped 67%. TBT dropped 71%. The reason TBT improved: software GIF decoding had been blocking the main thread on each frame. Hardware MP4 decoding offloads to a separate process.
Caveats and gotchas
- Autoplay needs muted. iOS and most browsers since 2018 will block any video with audio from autoplaying. Always set
mutedon autoplay loops. - iOS needs playsinline. Without it, iOS opens the video in a fullscreen player on tap.
- Even dimensions for H.264. The codec requires both width and height to be divisible by 2. Use the
scale=trunc(iw/2)*2:trunc(ih/2)*2filter. - Safari needs yuv420p. Other pixel formats decode in Chrome but fail silently in Safari.
- Don't use a video for static decoration. If your "animation" is barely animated (a slow zoom, a subtle pan), a still image or CSS animation is cheaper.
Beyond Lighthouse: real-user impact
The Lighthouse number is a synthetic score. The real metric to watch is field data — actual visitors on actual networks. We saw, in the two weeks following the change:
- p75 LCP in production: 3.8s → 1.6s (Chrome User Experience Report)
- Bounce rate: -8% on mobile (per GA)
- CDN bandwidth bill: -$340 / month for the marketing site alone
The bandwidth savings are dwarfed by the engineering hours we'd already burned trying to squeeze GIF further with palette tricks and dithering. None of that mattered once we crossed the format boundary.
How to do this on your own site
The five steps that took us from 64 to 92:
- Find every
.gifon the page in DevTools Network tab. - Convert each to MP4 (H.264) and WebM (VP9). For one-offs, our GIF to MP4 converter handles both formats and runs in the browser. For pipelines, use the ffmpeg commands above.
- Generate a still-frame JPEG poster for each video.
ffmpeg -i input.gif -vf "select=eq(n\\,0)" -vframes 1 poster.jpg. - Replace each
<img>with a<video>as shown above. - Re-run Lighthouse. Verify the score jumped. Verify LCP dropped.
Total time: roughly 20 minutes per page for sites we manage. The compounding effect on every page view across thousands of users makes it the single highest-leverage perf fix we've made all year.