Every "how to make a GIF with FFmpeg" answer on the internet contains the same
two-pass incantation: run your video through palettegen, then pipe it back
in with paletteuse. The comments under those answers say things like
"smaller file, better colors."
So when I needed to shrink some demo GIFs this week, I ran the trick — and
my files got bigger. Not slightly bigger. Up to 117% bigger, at identical
resolution and frame rate. So I benchmarked it properly, because either my
FFmpeg was broken or the folk wisdom was.
Spoiler: the folk wisdom conflates two different claims. The palette trick
buys you quality. It does not buy you size. And once you know that, the
actual size levers become obvious — they're just not the ones the
copy-pasted answers emphasize.
The setup
Three synthetic 10-second clips, 1280×720, 30 fps, so everything is
reproducible and nothing depends on a specific camera's noise profile:
# screen-recording-like motion (sliders, bars, text)
ffmpeg -f lavfi -i testsrc2=size=1280x720:rate=30:duration=10 \
-c:v libx264 -pix_fmt yuv420p -crf 20 screen.mp4
# smooth color gradients, slowly zooming (worst case for a 256-color format)
ffmpeg -f lavfi -i mandelbrot=size=1280x720:rate=30 -t 10 \
-c:v libx264 -pix_fmt yuv420p -crf 20 gradient.mp4
# hard edges, few colors (cellular automaton — the "flat UI animation" profile)
ffmpeg -f lavfi -i life=size=1280x720:rate=30 -t 10 \
-c:v libx264 -pix_fmt yuv420p -crf 20 blocks.mp4
FFmpeg 8.0 on macOS. Five conversion recipes per clip:
| Recipe | |
|---|---|
| A | the naive one-liner: fps=15,scale=640:-1, no palette pass |
| B | the famous trick: two-pass palettegen=stats_mode=diff + paletteuse=dither=sierra2_4a, same fps=15,scale=640:-1
|
| C | B, but fps=10
|
| D | B, but scale=480:-1
|
| E | B at native 1280×720, 30 fps (the "just convert it" ceiling) |
For the record, the two commands behind B:
ffmpeg -i in.mp4 -vf "fps=15,scale=640:-1:flags=lanczos,palettegen=stats_mode=diff" pal.png
ffmpeg -i in.mp4 -i pal.png -lavfi "fps=15,scale=640:-1:flags=lanczos [x];[x][1:v] paletteuse=dither=sierra2_4a" out.gif
Finding 1: the palette trick is a quality feature, not a compression feature
All three clips, same filters, the only difference is the palette pass:
| Clip | A (naive) | B (palette) | B vs A |
|---|---|---|---|
| screen | 2.05 MB | 3.37 MB | +64% |
| gradient | 10.95 MB | 17.56 MB | +60% |
| blocks | 5.87 MB | 12.72 MB | +117% |
The palette version is bigger. Every time. Why? Two reasons, both measurable.
Reason one: dither noise. sierra2_4a is an error-diffusion dither. It
sprays high-frequency noise across every pixel that isn't exactly
representable in the 256-color palette — and GIF's LZW compression despises
high-frequency noise. Switching the same palette pass to ordered dithering
(dither=bayer:bayer_scale=5) claws some of it back (3.37 → 3.03 MB on the
screen clip) but never gets close to the naive path.
Reason two: the naive path quietly throws colors away. Count the
distinct colors in the first frame of each output:
| Clip | A (naive) | B (palette) |
|---|---|---|
| screen | 184 | 236 |
| gradient | 174 | 255 |
| blocks | 18 | 221 |
That blocks row is the whole story in miniature. The cellular-automaton
clip is mostly flat white and black with a handful of accent colors — and
the default quantizer crushes it down to 18 effective colors, which LZW then
compresses beautifully. The content-aware palette preserves 221 of them,
which looks better and compresses worse.
So this is not a bug and I'm not dunking on the technique. If your source
has gradients — screen recordings with soft shadows, anything photographic —
the naive path will band into ugly stair-steps and the palette pass genuinely
fixes that. But if you're reaching for palettegen "to make the file
smaller," you've been mislabeled by the comments section. It's a
quality-for-size trade, priced at roughly +60–117%.
Finding 2: the levers that actually move size are fps and width
Same palette recipe (B), one knob at a time:
| Clip | B (640px, 15fps) | fps→10 | width→480 |
|---|---|---|---|
| screen | 3.37 MB | 2.32 MB (−31%) | 2.30 MB (−32%) |
| gradient | 17.56 MB | 11.92 MB (−32%) | 10.19 MB (−42%) |
| blocks | 12.72 MB | 8.85 MB (−30%) | 8.43 MB (−34%) |
Dropping from 15 to 10 fps costs you a third of the size across the board,
and for a UI demo or a loading animation, 10–12 fps is usually plenty —
cursor trails get slightly choppier, nothing else changes. Width is the
sharper lever on gradient-heavy content (−42%), because GIF stores every
pixel of every frame; fewer pixels per frame compounds over 300 frames.
Stack both (10 fps, 480px) and the gradient clip lands under 8 MB from a
17.6 MB starting point — a bigger win than any palette-vs-no-palette choice,
with zero quality debate.
Finding 3: know when GIF is simply the wrong format
Recipe E converts at native 1280×720, 30 fps, full palette. The gradient
clip — 10 seconds, remember — comes out at 128.6 MB. The MP4 it was made
from is 9.7 MB. That's 13× the size for objectively worse color (256 colors
dithered vs. the original).
(Small honesty note: on the noisy blocks clip the native-res GIF was 17.8
MB against a 40.8 MB CRF-20 MP4 — GIF can "win" against a high-bitrate
noisy source. But that's the exception profile: flat colors, zero gradients.
If your content looks like a photograph or has any smooth shading, it is not
that profile.)
Modern answer for "a GIF but sane": MP4 or WebM in a <video> tag, or an
animated WebP/AVIF if you need a single self-contained image file. Reserve
actual GIF for the places that force it — README badges, Slack, email,
that one legacy portal. And when it is forced, that's exactly when the
fps/width discipline above earns its keep.
Recipes, by what your clip looks like
-
Screen recording / UI demo (flat colors): naive path, no palette —
fps=12,scale=640:-1. Smallest file, and flat content survives the default quantizer just fine (184 colors on my test clip, no visible loss). -
Anything with gradients or photographic content: palette pass with
dither=bayer:bayer_scale=5, then claw size back withfps=10and 480–640px width. Accept that quality costs bytes here. -
Both kinds, and you need it under a hard limit (say an email
attachment or a 10 MB upload cap): drop to
fps=10,scale=480:-1and trim the clip to the 3–5 seconds that matter. Cutting duration is the one lever stronger than every other lever.
One more practical note: if the clip is a screen demo, check what's in it
before uploading it to any converter — cursor paths and notifications leak
context. The converter I keep bookmarked for this
(iLoveConvert's MP4 to GIF page)
auto-deletes uploads within 24 hours and doesn't demand an account, which is
the baseline I hold any such tool to, but "deleted" doesn't help with a
screenshot of a customer record that's now in a GIF you pasted into a public
issue thread.
Takeaway
The two-pass palette recipe is good FFmpeg advice with a bad reputation. It
makes GIFs look better and weigh more. When your problem is weight, the
cheap wins are boring: fewer frames, fewer pixels, fewer seconds. Measure
instead of trusting the comments — it's two commands and a table like the
one above.
I benchmark file-format tooling and write up the numbers — previously:
500-image PNG→JPG conversion benchmarks, batch audio extraction from video,
and five ways to turn PDF pages into images, all with reproducible scripts.
Top comments (0)