Most online GIF makers upload your video to a server, add a watermark, and bury the download button under ads. I wanted the opposite, so I built Snipgif: a free, open-source video-to-GIF converter where your file never leaves your device.
What it does
- Convert MP4, MOV, WebM, or AVI to GIF
- Trim on a timeline (up to 30 seconds of GIF)
- Presets (Small for chat, Balanced, High quality) or custom FPS, width, and quality
- Crop to 1:1, 16:9, or 9:16, change speed, reverse, or boomerang
- Meme-style captions
- A standalone GIF compressor (fewer colors, skipped frames, smaller width)
- Installable as an app, and it works offline
How it works
The whole conversion runs in the browser with ffmpeg.wasm. The site itself is a static Astro build. Only the converter and compressor are React islands, so the other pages ship almost no JavaScript.
A few decisions that mattered:
1. Load the 30 MB engine only when needed. The ffmpeg core is big. It isn't part of the page load. It downloads from jsDelivr the moment you pick a file, so by the time you hit Convert it's usually ready.
2. Single-threaded core on purpose. The multi-threaded ffmpeg.wasm core needs SharedArrayBuffer, which means COOP/COEP headers. Those headers block third-party resources that don't opt in. The single-threaded core is slower, but it keeps hosting simple.
3. Offline with a hand-written service worker. At build time, a small Astro integration writes the full list of built files into sw.js, so every page is cached on install. The ffmpeg core is cached the first time you use it.
One bug worth sharing: my first version did await cache.put(...) before returning the response. If the browser refused to store the 30 MB file (a phone low on storage, for example), a successful download turned into an error. The fix was to make caching best-effort: return the response right away and do the cache write in event.waitUntil(...), swallowing errors. A cache should make things faster, never break them.
4. Privacy without a cookie banner. Analytics are optional (GA4). Google Consent Mode defaults to denied for visitors in the EEA, the UK, and Switzerland, so they get no analytics cookies and no banner is needed.
Stack
Astro, React, TypeScript, Tailwind CSS, shadcn/ui, ffmpeg.wasm, deployed on Vercel.
Try it or contribute
- Live: https://snipgif.vercel.app
- Code (MIT): https://github.com/saidMounaim/snipgif
Issues and pull requests are welcome. I'd love feedback, especially on performance with large videos on mobile.
Top comments (3)
That cache.put bug is nasty, a quota refusal on some random phone is the kind of thing you'd chase for hours. Curious how you fixed it, skip the cache when the write fails and eat the 30MB download again next run?
On large files on mobile, the first thing I'd check is how the video gets into ffmpeg.wasm. Writing it with writeFile copies the whole file into wasm memory, so a long phone recording can use up the tab before decoding even starts. Version 0.12 can mount the File through WORKERFS and read it in place, which helps a lot on low memory phones. I'd also put -ss and -t before the input so only the trimmed range gets decoded, then scale down before palettegen rather than after. Loading the 30 MB core only when someone picks a file is a nice touch. And making the cache write best effort was the right fix, because a failed cache shouldn't turn a good download into an error.
Handling the single-threaded build to bypass COOP/COEP is a smart trade-off for static hosting. SharedArrayBuffer requirements create endless headaches with third-party CDNs and embedded previews.
Two browser memory quirks we ran into with client-side media pipelines might be relevant as you test larger files on mobile:
The event.waitUntil fallback for the 30 MB core cache write is a great catch. Service workers silently rejecting full quotas on budget Android phones is a notorious edge case that rarely shows up in desktop dev tools.