blog

Why gifski beats ffmpeg for GIFs.

2026-04-22 · by alain

The two-line answer.

ffmpeg, by default, gives the whole GIF one 256-color palette. gifski gives every frame its own 256-color palette. That's the entire delta — and it shows up everywhere color changes over time.

ffmpeg default:   [frame1, frame2, frame3, ...]  →  ONE palette of 256 colors  →  GIF
gifski:           [frame1]  →  palette A (256 colors)
                  [frame2]  →  palette B (256 colors)
                  [frame3]  →  palette C (256 colors)
                                                   →  GIF (per-frame palettes)

Why this matters.

GIF is a palettized format. Each frame indexes into a color table capped at 256 entries. That cap is baked into the spec and it is not moving. So the interesting question for any GIF encoder is: which 256 colors do I pick, and how often do I pick?

The one-palette approach is cheap. You sample every frame, build one shared palette, then encode every frame against it. For static content — a logo animation, a loop with two background colors — this is fine. The palette captures the colors actually used, and you're done.

Real screen recordings are not that. A cursor moves across a gradient button, a dropdown paints over a background image, a video of a game pans across grass and sky. Each frame has slightly different colors than the frame before it. If the whole thing has to share one palette, every frame compromises — you end up with banding, posterization, or dithered noise papered over everything that changes. The shared palette picks the colors that are most common across the whole clip; the less-common colors get nearest-matched and lose their integrity.

Think about what a global palette actually encodes: an average of every frame's color needs. A frame that's 70% dark navy and a frame that's 70% burnt orange are forced to share the same 256 slots. Whichever hues the encoder picks, one of the two frames gets shortchanged. The per-frame approach refuses the average. Each frame gets the 256 colors that fit it. The GIF is bigger as a result — two palettes are heavier than one — but gifski's Rust encoder pays that bill with tight, per-palette compression that keeps the total well under what a naive implementation would produce.

How gifski works.

gifski is Kornel Lesiński's encoder, written in Rust, the same engine that powers Sindre Sorhus's gifski.app on Mac. Under the hood it uses NeuQuant — a neural-net-based color quantizer — to pick an optimal 256-color palette for each frame independently. Then it writes those frames as a single GIF stream, one palette per frame (the spec allows it; it just rarely gets used).

The second thing gifski does is temporal dithering. For frames where two similar colors would otherwise band, gifski distributes the error across adjacent frames in time — so the noise is moving, not static. Human vision integrates moving noise back into smoothness. A still frame of a gifski GIF might look a little speckled if you pause and squint; played back at 24 or 30 fps, it looks clean.

The third thing is that it does not try to be clever about inter-frame diffing the way modern video codecs do. GIF has frame-disposal semantics (replace / composite / restore), but the quality win from per-frame palettes dominates any gain from trying to re-use pixel buffers across frames. gifski plays that straight.

The cost is encode time. Per-frame palette generation is more work than one-shot palette generation, and temporal dithering adds passes. A 30-second 1080p clip that ffmpeg single-pass encodes in four seconds might take gifski twelve. For a GIF you're going to paste in a pull request and look at for the next six months, twelve seconds is nothing.

There's one more piece worth naming: gifski respects the GIF spec strictly. Some older encoders play games with LZW table resets or frame-disposal flags to shave bytes, and some players render those tricks inconsistently. gifski's output plays identically in Chrome, Firefox, Safari, Discord, Slack, Photoshop, and the Windows photo viewer — which sounds like a low bar until you've seen the alternative. If you've ever sent a GIF that looked fine for you and broken for the person you sent it to, a non-conforming encoder was probably involved.

Where it shows up most.

Screen recordings of UI. A Figma file scrolling over a colored background, a VS Code window with syntax highlighting and an animated cursor, a browser dev tools panel expanding — these are the clips where ffmpeg-default GIFs look muddy and gifski GIFs look crisp. The UI has a lot of colors (every syntax-highlighted token is a different hue), and the colors in motion change frame-to-frame.

Color-rich game footage. A pan across a sunset, a spell effect with particle colors, any kind of bloom or glow. Single-palette encoding smears the transitions; per-frame palettes preserve them.

Gradient-heavy designs. Marketing animations, onboarding screens with background gradients, data visualizations with continuous color scales. These are the worst case for the single-palette approach — the gradient gets quantized to a handful of bands, and the bands shimmer as the camera pans. Per-frame palettes don't fix banding entirely (you still only have 256 colors per frame), but they let each frame pick the 256 colors that matter for that frame. Combined with temporal dithering, the shimmer becomes a soft, moving grain instead of a stepped boundary — much closer to what the source actually looked like.

The flip side: gifski is not obviously better on content that barely moves or has very few colors. A two-color logo looping on itself, a line drawing animation, a wireframe demo — the single-palette approach is already optimal there, because one palette is what the content needs. Use gifski anyway and you get slightly larger files with no visible quality gain. That's fine. Nobody's trying to save 40 KB on a logo loop. The win shows up the moment you add a gradient, a shadow, a photograph, or a second scene.

The ffmpeg palettegen + paletteuse two-pass workaround.

To be fair, ffmpeg can make decent GIFs — but you have to do the two-pass dance. First pass generates a palette with stats-mode set to diff (which weights the palette toward frames with the most change). Second pass uses that palette with a good dither option:

ffmpeg -i input.mp4 -vf "fps=15,scale=720:-1:flags=lanczos,palettegen=stats_mode=diff" palette.png
ffmpeg -i input.mp4 -i palette.png -lavfi "fps=15,scale=720:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=sierra2_4a" output.gif

That's noticeably better than single-pass ffmpeg. It's still one global palette, though — just a smarter one, weighted toward change. For a short clip with stable content, the difference versus gifski is small. For a longer clip or anything with big color shifts across time, gifski still wins because it isn't making a compromise at all. The palettegen approach is a better compromise; per-frame palettes skip the compromise.

What gifcap does with it.

gifcap bundles gifski and ffmpeg and wires them together so you don't have to. ffmpeg decodes and scales your source video (that's what it's best at); gifski encodes the final GIF (that's what it's best at). You drop an MP4 onto the window and pick your dimensions, fps, and a target file size. gifcap does the rest.

The gifski-for-Windows binary is baked into the installer — you don't need to track down builds. If you're evaluating encoders, the best GIF encoder on Windows walks through the contenders; versus ScreenToGif covers the same-engine peer that ships WPF-based tooling around the same core.

The other thing gifcap layers on is a hard size cap. gifski's quality slider is great, but it's still a slider — you pick a number, you encode, you see if it fit, you retry. gifcap binary-searches that slider for you. Type "5 MB" and gifcap converges on the highest quality that lands under 5 MB, typically in five to seven iterations. That piece is a whole separate post.

Try it.

Free tier covers most of it. Pro is $29 lifetime for scene detection and the gallery.

Download gifcap free more blog posts 18 mb installer · signed · windows 10/11