GIF is from 1987 and it is still how most people share a short loop. It is also famously wasteful: the same few seconds are usually several times larger as a GIF than as a video. This article opens the format up, step by step, and measures what each setting of the GIF maker does to one real clip. The numbers come from running the tool's own encoder on that clip, not from a rule of thumb.
The test clip
To have something repeatable, I made a 3-second clip with FFmpeg's testsrc2 pattern: 480 × 270 pixels, 10 frames per second, so 30 frames. It has a moving colour gradient, scrolling text and a ticking counter, which makes it a hard case, closer to busy footage than to a screen recording. Encoded as H.264 MP4 at FFmpeg's default quality (CRF 23) it is 106 KB.
The same 30 frames as raw pixels are 15.6 MB (30 × 480 × 270 × 4 bytes). Everything below is about how a GIF gets from 15.6 MB down, and why it stops well above 106 KB.
Step 1: at most 256 colours per frame
A GIF frame does not store colours, it stores indices into a colour table of at most 256 entries. That turns 4 bytes per pixel into 1 byte (or less) before any compression, but a photo or a gradient has tens of thousands of colours, so they have to be reduced first.
The GIF maker picks the palette by median cut: it counts the pixels in a histogram, puts all colours in one box, and keeps cutting the box with the largest spread in two at the median of its widest channel until there are as many boxes as palette entries. Each box becomes one colour: the average of the pixels in it. Then every pixel is replaced by the nearest palette colour.
Two choices matter here:
- One palette for the whole animation or one per frame. A shared palette is written once; a per-frame palette costs up to 768 bytes per frame and can make colours flicker when a pixel maps to slightly different colours in consecutive frames.
- How many colours. Fewer colours mean fewer distinct indices, longer runs of the same index, and better compression.
Step 2: LZW compression
Each frame's indices are compressed with LZW (Lempel–Ziv–Welch, 1984). It starts with a dictionary of single indices and, as it reads, adds every new sequence "what I have so far plus the next index" as a new code. Repeated patterns soon become single codes. Codes start at the palette's bit width plus one and grow to at most 12 bits; when the 4096-entry dictionary is full, a clear code starts over.
A tiny worked example from the encoder's tests: four pixels of colour 0 in a 2-colour image. The minimum code size is 2, so the clear code is 4, the end code is 5 and codes are 3 bits wide. The encoder emits 4 (clear), 0, then 6 (the new code for "0, 0"), then 0, then 5 (end). Packed least significant bit first, that is 4 + 0·8 + 6·64 + 0·512 + 5·4096 = 20,868, or the two bytes 84 51. Four pixels, two bytes.
LZW is excellent at flat areas and long runs, and nearly useless on noise: a pixel that differs from its neighbour breaks the run.
What the settings did to the clip
All sizes are the complete GIF file from the tool's encoder, same 30 frames:
| Setting | Size |
|---|---|
| 256 colours, one palette per frame, whole frames | 457 KB |
| 256 colours, shared palette, whole frames | 425 KB |
| 256 colours, shared palette, store only what changes | 356 KB |
| 64 colours, shared palette, store only what changes | 222 KB |
| 256 colours, shared, changes only, dithering on | 481 KB |
For comparison, FFmpeg's own GIF output with an optimised palette and its default dithering came to 530 KB, and the MP4 was 106 KB. Even the best GIF setting here is about twice the MP4, and the 256-colour default about 3.4 times. MP4 wins because it predicts each frame from the previous one and only codes the difference, with motion vectors and a transform designed for natural images; GIF compresses every frame on its own.
Step 3: store only what changes
GIF has two features that make a cheap form of inter-frame compression possible: a frame can be smaller than the screen and placed anywhere on it, and a frame can mark one index as transparent so that what is already on screen shows through. With the "leave in place" disposal method, the next frame is drawn on top.
So after the first frame, the GIF maker stores only the bounding rectangle of the pixels that change, and inside it turns pixels that look the same as before into the transparent index. Long runs of "transparent" compress to almost nothing. On the busy test clip that saved 16% (425 → 356 KB), because almost everything moves.
On a mostly still clip the effect is dramatic. I made a second version where only a 100 × 40 pixel area changes (like a ticking timer on a slide) and the rest stays still: 433 KB as whole frames, 24 KB with "store only what changes". Screen recordings, UI demos and talking heads behave like this.
Step 4: timing
Each frame has a delay in hundredths of a second, and there are two traps:
- Browsers treat 0 and 1 hundredth as 10 hundredths. A GIF "at 100 fps" plays at 10 fps. The shortest delay that really works is 2 hundredths, 50 fps. The tool never writes less.
- Rates that do not divide 100. 15 fps is 6.67 hundredths per frame, which cannot be stored. Rounding every frame to 7 makes a 1-second clip last 1.05 s; the tool carries the remainder instead, writing 7, 6, 7, 7, 6, 7... so 15 frames take exactly one second.
The loop is a separate block (NETSCAPE2.0) with a repeat count: 0 means forever, and "play three times" is written as 2 repeats.
When to use something else
If the destination plays video, use video. An MP4 or WebM is smaller and smoother, and has sound. Animated WebP is also much smaller than GIF and plays in every current browser, but many chat apps and older programs still only animate GIF, which is why GIF survives.
Common mistakes
- Exporting at full HD. Width costs twice: 960 px wide has four times the pixels of 480. For chat, 320 to 480 px is plenty.
- Too many frames per second. 10 to 15 fps reads as smooth for most loops; 30 fps doubles or triples the size.
- Dithering screenshots. Dithering helps gradients and photos; on flat UI it only adds noise (on the test clip it added 35%).
- Per-frame palettes by default. Use them only when scenes are very different; a shared palette is smaller and does not flicker.
- Long clips. A GIF of 20 seconds is almost always the wrong format. Record a video with the screen recorder instead, or cut the loop to its essential three seconds.
Recipe
- Keep the clip to 3 to 5 seconds in the GIF maker.
- Width 480 px (320 for stickers), 10 to 12 fps.
- Shared palette, "store only what changes" on.
- Try 128 or 64 colours; turn dithering on only if you see bands in gradients.
- Check the estimate, make the GIF, and compare it with the source before you send it.