Your GIF Is the Lossy One
Sep 14, 2026

Your GIF Is the Lossy One

728 Words|4 Minutes to read

One of my project pages was loading 18MB of images.

Not a slow page. A page that eats a fifth of someone's daily mobile data to show them five looping demos.

The demos were GIFs. One of them, a sunrise animation, was 6.85MB by itself.

I converted it expecting to trade quality for size:

ffmpeg -i sunrise.gif -movflags faststart -pix_fmt yuv420p \
-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
-c:v libx264 -crf 23 -preset slow -an sunrise.mp4

6.85MB became 0.32MB. Twenty-one times smaller. Then I pulled a frame from each to see what I'd lost, and found I'd got it backwards.

Count the colours

Same frame, same crop, from both files:

GIF frame: 243 unique coloursMP4 frame: 16,879 unique colours

GIF caps a frame at 256 colours. That's not a setting you can turn up. It's the format, fixed since 1987. A photo-ish gradient has to be dithered down to fit, which is why the dark parts of that sunrise are visibly stippled and the MP4 isn't.

So the GIF was the lossy file. Lossy and twenty-one times bigger.

Why it's bigger

Two reasons, both baked into the format.

No interframe prediction. GIF compresses every frame on its own with LZW. H.264 stores one keyframe and then the differences — motion vectors and residuals. A looping card animation where the background never moves is almost free in a video codec. In a GIF you pay for the whole picture, every frame.

Dithering kills the compression. Squeezing a smooth gradient into 256 colours makes noise, and noise barely compresses. The workaround for the colour limit is what blows up the file size.

All six files

FileGIFMP4Ratio
eid-doodle-crescent4.03MB0.21MB19x
eid-salami3.52MB0.13MB28x
get-well-sunshine3.45MB0.17MB20x
new-year-fireworks2.17MB0.15MB15x
pohela-boishakh-sunrise6.85MB0.32MB22x
v2-dev-progress1.20MB0.22MB5x
Total21.2MB1.19MB18x

The 5x one is a screen recording of a UI. Flat colours, not many of them, which is the case GIF was actually built for. It still loses.

Page after the swap, measured on the build: media went from ~18MB to 1.53MB, posters included.

Making it behave like a GIF

The thing people want from a GIF is four attributes:

<video
src={src}
poster={poster}
autoPlay
loop
muted
playsInline
preload='metadata'
/>

muted and playsInline do the heavy lifting. iOS won't autoplay anything with audio, and without playsInline it yanks the video fullscreen when you tap it. The poster is a still frame so you don't get an empty black box on a slow connection.

One bug worth knowing if you put these in a carousel: autoPlay only applies when the element mounts. I had a rail of six clips with autoPlay={i === selected} and five of them played at once, because nobody ever told the ones you scrolled past to stop. You have to drive it:

useEffect(() => {
const el = ref.current;
if (!el) return;
if (active) {
void el.play().catch(() => {});
return;
}
el.pause();
el.currentTime = 0;
}, [active]);

Then why is everything still called a GIF

Mostly it isn't one.

X converts your uploaded GIF to MP4 on upload. Giphy and Tenor hand apps .mp4 and .webm and keep .gif around for dumb embeds. WhatsApp stickers are animated WebP. Telegram's are Lottie or WebM. The word stuck, the format didn't.

Real GIF still wins in one place: where a plain <img> is all you get. Email, old forums, a GitHub README. Everywhere else the tag you want is <video>.

And if you're keeping GIFs because they autoplay without a player — that argument stopped being true around 2017, when muted + playsInline started working properly on phones.

  • performance
  • video
  • ffmpeg
  • gif
  • web performance

Comments