How to Convert GIF to MP4 with FFmpeg (and Cut Page Weight 90%)

Someone uploads a 12 MB animated GIF to your app and your page weight doubles. You know MP4 would be a fraction of the size, but your first conversion either refuses to encode, plays as a gray smear in Safari, or shows a play button instead of looping silently like the GIF did.
Quick answer: To convert GIF to MP4 with FFmpeg, runffmpeg -i input.gif -movflags +faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" output.mp4, then embed it with<video autoplay loop muted playsinline>so it behaves like the original GIF. Those three flags are what keep the file from failing on odd pixel dimensions or playing back wrong on iOS, and the MP4 typically lands 85 to 95 percent smaller than the GIF it replaced. If the GIFs arrive as user uploads and you'd rather not put an encoder in your request path, FFmpeg Micro runs the same conversion as one API call with no servers to run.
A GIF isn't heavy because it's long. It's heavy because it can't predict motion.
The size problem with GIF comes from its compression model, not its duration. GIF stores each frame as its own LZW-compressed image limited to a 256-color palette, with only frame-level differencing to reuse pixels between frames. H.264 does motion compensation: it encodes one keyframe and then describes later frames as blocks that moved, plus a small residual.
That difference is why a 5-second screen recording exported as GIF can be 8 MB and the same 5 seconds as MP4 is 300 KB. Google's web.dev guide on replacing animated GIFs with video runs the same math, but stops before the pipeline. The conversion is one command. What trips people up is that three defaults in it are wrong for the web.
Dithering makes it worse. Dither noise is the hardest thing you can hand to a video encoder, because it's high-frequency detail that looks random to motion estimation. A dithered GIF converted at -crf 23 can come out larger than a clean one at half the file size. If you control the GIF export too, our palette method for MP4-to-GIF covers the other direction.
The three flags that decide whether the MP4 plays everywhere
Every flag in this command is there because leaving it out breaks something specific:
ffmpeg -i input.gif \
-movflags +faststart \
-pix_fmt yuv420p \
-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
-c:v libx264 -crf 23 -preset slow \
output.mp4
-pix_fmt yuv420p
A GIF decodes to RGB or 8-bit palette, and x264 may otherwise pick a chroma format that consumer hardware decoders won't touch. -pix_fmt yuv420p forces 4:2:0 chroma subsampling, which is the only format QuickTime, Safari, and most hardware decoders reliably accept. Skip it and the file plays fine in VLC and Chrome on your desktop, then shows a black frame on an iPhone. Your test environment says it works, which is why this one wastes an afternoon.
scale=trunc(iw/2)*2:trunc(ih/2)*2
H.264 with 4:2:0 chroma requires even pixel dimensions on both axes, and GIFs are routinely odd because they were cropped by hand. The trunc(iw/2)*2 expression rounds width and height down to the nearest even number, dropping at most one row and one column. Without it, a 500x373 GIF dies at encoder init:
[libx264 @ 0x55d0d8f2] height not divisible by 2 (500x373)
Error initializing output stream 0:0 -- Error while opening encoder for output stream #0:0
If you're also resizing, use -2 rather than -1 for the auto-computed side. scale=800:-2 scales to 800 pixels wide and picks the nearest even height.
-movflags +faststart
An MP4 stores its index, the moov atom, at the end of the file by default, so a browser has to download the whole thing before it can start playing. -movflags +faststart moves that atom to the front in a second pass. For a 300 KB file on a fast connection nobody notices. For a 4 MB screen recording on mobile data, it's the difference between playback starting in half a second and starting after the full download.
The markup that makes an MP4 behave like a GIF
Replacing a GIF means replacing its behavior, not just its bytes: autoplay, loop forever, no sound, no controls, no fullscreen hijack on iOS.
<video autoplay loop muted playsinline poster="poster.jpg" width="600">
<source src="clip.mp4" type="video/mp4">
</video>
Chrome and Safari will not autoplay a video that has sound, so muted is what makes autoplay actually fire. playsinline stops iOS Safari from taking the video fullscreen the moment it starts. loop restores the endless repeat, and poster gives you a first frame while the file is still arriving.
Set width (or a CSS width) explicitly. A GIF gets its intrinsic size from the image; a <video> that hasn't loaded metadata has none, so leaving it off gives you a layout shift on every page load and a Core Web Vitals complaint a month later.
MP4 alone is enough in 2026. Adding a WebM source with -c:v libvpx-vp9 -crf 34 -b:v 0 saves another 20 to 30 percent. Worth it if you serve the same asset a million times, but it doubles storage and encode time for a file most visitors will never receive.
Converting a queue of uploaded GIFs at ingest
Converting one GIF is a command. Converting every GIF that lands in your app is a pipeline decision:
| Approach | Setup cost | What breaks | Good for |
|---|---|---|---|
| FFmpeg in your app server | Install a binary, pin a version | Encoding blocks a request thread; a 40 MB GIF spikes CPU for everyone | A CLI on your own machine |
| FFmpeg in a Lambda/Cloud Run worker | Layer or container, plus a queue | Cold starts, timeouts, per-invocation memory tuning | Teams already running a job queue |
| A hosted media API | An API key | Nothing you own | Uploads at unpredictable volume |
The middle row is where most people end up, and it's fine until the day someone uploads a 200 MB GIF export from After Effects and your worker's 60-second timeout kills it halfway. We wrote up the full cost comparison of running FFmpeg yourself versus in the cloud.
If you want to skip the encoder entirely: FFmpeg Micro takes the same FFmpeg job as one REST call, runs it on managed infrastructure, and hands back a URL for the MP4. You submit the job, take the webhook or poll for it, then download the output. No binaries to install, no version drift between your laptop and production, no long-running request to babysit. The docs have the job schema, and the playground will run a GIF through it before you write any code.
A working ingest flow, whichever way you run the encode:
- Accept the upload and store the original GIF; you'll want it when you change encoder settings.
- Queue a conversion job keyed by the asset ID.
- Run the command above, plus
scale=800:-2if you're capping display width. - Extract a poster frame with
ffmpeg -i input.gif -frames:v 1 poster.jpg. - Write the MP4 URL, poster URL, and pixel dimensions back to your record so the template can render
<video>without a layout shift.
The same shape works in n8n, Make, or Zapier: watch a folder or a webhook, call the API, write the result back to a database row.
Pitfalls that only show up on real GIFs
Test files are clean. GIFs from the internet are not, and three problems account for most conversion bugs.
Transparency turns black. MP4 has no alpha channel, so any transparent GIF region encodes as black. Flatten it onto a background first:
ffmpeg -i input.gif -filter_complex \
"[0:v]format=rgba,split[bg][fg];[bg]drawbox=c=white:t=fill[base];[base][fg]overlay=shortest=1,format=yuv420p" \
-movflags +faststart output.mp4
That paints a white frame the size of the source and composites the GIF over it. Change c=white to match your page background.
Frame timing drifts. GIF stores per-frame delays in hundredths of a second, and very short delays of 0 or 1 get clamped by browsers to roughly 10 fps. FFmpeg reads the file's stated timing instead, so a GIF that looked correct in the browser can come out noticeably faster as MP4. Check the source with ffprobe -v error -select_streams v -show_entries stream=r_frame_rate,nb_frames input.gif, and pin the output with -r 15 if the result looks sped up.
Screen-recorded GIFs are enormous in resolution. A GIF captured from a Retina display can be 2800 pixels wide and displayed at 700. Add scale=800:-2 before the even-dimension trick (or just use it instead, since -2 already enforces the rule) and you'll cut the file again by more than the codec change did.
One more: don't run -crf 18 on GIF-sourced content out of caution. GIF is already palette-quantized, so there's little detail left to preserve below about -crf 23. A lower CRF mostly buys you a bigger file that faithfully reproduces dither noise.
When you should leave the GIF alone
GIF is still the right format when the destination can't play video. Email is the clearest case: animated GIFs play in Apple Mail and most webmail clients, while a <video> tag renders as nothing in Outlook, so marketing emails stay on GIF regardless of file size. Some chat and forum embeds are the same story.
Size matters too. A 4-frame, 20 KB reaction GIF gains nothing from conversion, and you've added an encoding step plus a second asset to manage. The rule worth applying at ingest: convert anything over about 500 KB, leave the rest.
And if you have three GIFs to convert, run the command locally and move on. An API earns its keep when conversions are continuous and unpredictable.
FAQ
Is MP4 always smaller than the same GIF?
MP4 is smaller than the equivalent GIF in nearly every real case, usually by 85 to 95 percent, because H.264 encodes motion between frames while GIF re-encodes each frame as a separate image. The exception is animations of a few frames, where MP4 container overhead is a meaningful share of an already tiny file.
Why does my converted MP4 play in Chrome but not on iPhone?
An MP4 that plays in Chrome and fails on iPhone is almost always missing -pix_fmt yuv420p, which forces the 4:2:0 chroma subsampling that iOS hardware decoders require. The next most common cause is missing playsinline, which makes iOS Safari refuse inline autoplay and go fullscreen instead.
Can I convert GIF to MP4 without installing FFmpeg?
You can convert GIF to MP4 without installing FFmpeg by sending the file to a hosted media API that runs FFmpeg for you, which is how most teams handle user-uploaded GIFs. FFmpeg Micro exposes it as one REST call, and there's a free tier to check the output against a local encode.
Does converting GIF to MP4 lose quality?
Converting GIF to MP4 does not usually lose visible quality, because the GIF was already reduced to 256 colors and the H.264 encoder has more color depth to work with than the source contains. What can degrade is heavy dither patterns, which show up as fine noise that compression smooths out. Encoding at -crf 20 instead of -crf 23 fixes it if you can see it.
How do I convert a whole folder of GIFs at once?
To convert a folder of GIFs, loop the command in bash: for f in *.gif; do ffmpeg -i "$f" -movflags +faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "${f%.gif}.mp4"; done. For folders that keep filling up rather than a one-time run, batch transcoding through a workflow handles retries and failures better than a shell loop does.
If your app takes GIF uploads, you can point an API call at the file at ingest and get the MP4 back without adding an encoder to your stack. Sign up free and run your worst GIF through it.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Fix FFmpeg's "height not divisible by 2" Error on User Uploads
FFmpeg's "height not divisible by 2" error is a yuv420p chroma constraint, not a broken file. Three fixes (scale=-2, trunc, pad) with measured output sizes.

FFmpeg Two-Pass Encoding vs CRF: When It Actually Helps
FFmpeg two-pass encoding hits an exact file size that CRF can't guarantee. The -pass 1 and -pass 2 commands, when two-pass beats CRF, and the real pitfalls.

Stop Re-Exporting. Encoding a Video Once Covers Every Platform.
Encoding a video once and deriving every delivery version from it: the mezzanine command, CRF and preset per rung, social crops, and the one-call version.
Skip the command line
The Video to Animated GIF Converter blueprint runs the same conversion for you: upload the video, pick the length and width, download the GIF.
Run it (free)