Encode your demo recordings as h264, not GIF
TLDR
H264 beats GIF on file size, resolution, and frame rate at the same time, so switching costs you nothing. The trap is converting the GIF you already built, which permanently bakes in its colour quantisation. Encode from the original recording instead.I had a product walkthrough recorded as eighteen short clips, stitched into a highlight reel for the docs and marketing pages. The reel shipped as a GIF because that is what demo reels ship as. It weighed 7.6MB at 640 pixels wide and 10 frames per second, which is a lot of bytes for something blurry.
Encoding the same 65 seconds of source footage as h264 produced a 1.7MB file at 900 pixels and 24 frames per second. Smaller, sharper, smoother, all three at once.
GIF spends bytes on motion, and a moving camera is all motion
GIF has no real interframe compression. Each frame stores the pixels that changed since the last one, with no motion vectors and no prediction, so a moving camera invalidates the whole canvas on every frame. My recordings pan and zoom continuously, which is close to the worst input you can hand the format.
Every screencast has this property to some degree. Scrolling, animated transitions, and cursor movement all defeat the one compression trick GIF has.
h264 was built for exactly this. It predicts each frame from its neighbours and encodes the difference, so continuous motion is the case it handles well rather than the case that breaks it.
Here is the same source encoded both ways, measured in bytes:
| Encoding | Width | FPS | Bytes |
|---|---|---|---|
| GIF, 80 colours | 640 | 10 | 7,618,561 |
| GIF, 80 colours | 560 | 10 | 6,142,497 |
| GIF, 80 colours | 480 | 10 | 4,588,820 |
| h264, crf 23 | 900 | 24 | 2,467,871 |
| h264, crf 26 | 900 | 24 | 1,713,782 |
| h264, crf 30 | 900 | 24 | 1,063,136 |
Encode from the source recording, never from the GIF
This is where the obvious approach goes wrong, and it is the part worth remembering.
Converting an existing GIF to mp4 does shrink it. ffmpeg -i demo.gif demo.mp4 produces a much smaller file and looks like a win. What you actually get is a small file that permanently looks like a GIF.
Generating a GIF is lossy in three separate ways before you ever compress it. The palette collapses to 80 or 256 colours, the frame rate drops to something GIF can carry, and the width usually comes down to keep the weight sane. All of that is destroyed information. Encoding the result as h264 preserves the damage faithfully and cannot recover the colours, frames, or pixels that were thrown away.
The source recording still has them. Encoding from it is the same amount of work, one ffmpeg invocation either way, and it produces a genuinely sharp file rather than a compact record of a degraded one.
# Wrong. Small, but locked to 80 colours, 10fps, 480px forever.
ffmpeg -i reel.gif -c:v libx264 reel.mp4
Right. Same effort, full colour and detail retained.
ffmpeg -i source.mp4 \
-vf "scale=900:-2" \
-an -c:v libx264 -preset slow -crf 26 \
-pix_fmt yuv420p -movflags +faststart \
reel.mp4
Three flags in there earn their place. -pix_fmt yuv420p keeps the file playable in browsers and on iOS, which silently reject other pixel formats. -movflags +faststart moves the index to the front so playback begins before the file finishes downloading. -an drops the empty audio track that screen recordings carry.
The palette two-step disappears
A decent GIF needs two passes over the source. You build an optimised palette, then apply it, because the default 256 colour quantiser produces visible banding on UI gradients:
ffmpeg -i source.mp4 -vf "fps=10,scale=480:-1:flags=lanczos,palettegen=stats_mode=diff" palette.png
ffmpeg -i source.mp4 -i palette.png -lavfi "fps=10,scale=480:-1:flags=lanczos [x];[x][1:v] paletteuse=dither=bayer" out.gif
h264 replaces both passes with one command and no intermediate file. The encoder handles colour itself, so there is no palette to generate, tune, or accidentally reuse across clips.
One tuning note that saved me time. I tried -tune animation, expecting screen content to benefit, and it changed the output size by nothing measurable. Screen recordings of real UI are photographic enough that the animation heuristics do not apply. Pick a crf and move on.
Keep exactly one GIF, for markdown
Markdown cannot play video. If your README embeds a demo, and it renders on GitHub or a plugin marketplace, that surface needs a GIF and there is no way around it.
So keep one, and size it for that job. A README animation displays at a few hundred pixels in a narrow column, so 480 pixels is plenty and nobody will notice the frame rate. My GIF went from 7.6MB pretending to be a hero asset to 3.9MB doing an honest job as a thumbnail.
Everything that can render a <video> element should point at the mp4. That is every documentation site, every landing page, and every app surface you control.
Across the three assets I ship, a full walkthrough, a short reel, and the markdown fallback, this took the set from roughly 36MB to 17MB. The reel is the interesting one, because it got better and smaller in the same change.
Rule of thumb: never transcode a GIF you generated yourself. The source recording is still on disk, and encoding from it costs the same command.
Evidence
The benchmark table is real ffmpeg output measured on one 65 second source clip, encoded six ways in a single run. The two commands under "Encode from the source recording" are the actual working and broken forms, reduced to generic filenames.
Shipped sizes after the change, read off disk with stat:
walkthrough.mp4 11,900,784 bytes 272s 1280x800
walkthrough-reel.mp4 1,713,782 bytes 27s 900x562
walkthrough.gif 4,135,085 bytes 27s 480x300
The -tune animation result is a negative finding from a real attempt, not an assumption. Both encodes landed at 11MB for the full walkthrough, so the flag is noise for this content type.