DEV Community

Cover image for I built a file compressor that never uploads your files (ffmpeg.wasm + Canvas + Web Audio)
Jeffrey Hamilton
Jeffrey Hamilton

Posted on

I built a file compressor that never uploads your files (ffmpeg.wasm + Canvas + Web Audio)

I kept using online file compressors and realized they all upload your files to sketchy servers. For tax PDFs, personal photos, and sensitive documents — that didn't feel right.

So I built ShrinkRay — a file compressor that runs entirely in your browser. No backend, no uploads, no sign-up.

Try it: https://shrink-ray.vercel.app
Source: GitHub

What it compresses
Images: PNG, JPEG, WebP, AVIF, HEIC (iPhone photos), GIF, BMP
Video: MP4, WebM, MOV, MKV, animated GIF (via ffmpeg.wasm)
Audio: MP3, WAV, OGG, M4A, FLAC, OPUS
PDF: Re-renders pages as compressed images
3D Models: GLB, GLTF, STL, OBJ, PLY, FBX — with format conversion via Three.js
SVG: Minification (strips metadata, comments, whitespace)
Text/Code: JSON, HTML, CSS, JS, XML, YAML — minified + gzipped
Anything else: Gzip compression
How it works
File type
Engine
How
Video ffmpeg.wasm Compiled to WebAssembly, runs in-browser
Images Canvas API + OffscreenCanvas Re-encode at chosen quality/format
Audio Web Audio API + lamejs Decode → re-encode to MP3
PDF pdf-lib + pdfjs-dist Render pages → rebuild with compressed images
3D Three.js Format conversion (STL→GLB, etc.) + gzip
Text/Code Native JS + fflate Minify (strip comments/whitespace) → gzip

The architecture
Zero backend. It's a static site hosted on Vercel. There is no server — your files are processed entirely in the browser using WebAssembly and Canvas APIs.

text

Browser (your device)
├── Drop file
├── Auto-detect type (image/video/audio/pdf/3d/text)
├── Route to correct engine
├── Process entirely in memory (Blob URLs)
└── Download result
↳ Files NEVER touch a server
Key technical decisions

  1. Lazy-loaded heavy dependencies

ffmpeg.wasm is 32MB. We don't load it until you actually drop a video. Same for Three.js (3D), heic2any (HEIC photos), and pdfjs (PDFs). Initial page load stays fast.

  1. Multi-threaded ffmpeg when available

On browsers with SharedArrayBuffer (COOP/COEP headers), ffmpeg uses multiple CPU cores via Web Workers. Falls back to single-threaded automatically.

  1. Pre-warm on desktop, skip on mobile

On desktop, ffmpeg starts loading the moment the page opens (so it's ready when you drop a video). On mobile, we skip this — the 32MB download would freeze low-RAM devices.

  1. Never return a larger file

If compression makes a file bigger (happens with already-compressed files), we return the original instead. You never download a file larger than what you uploaded.

  1. Mobile memory safeguards

Mobile devices get stricter file size limits (100MB for video vs 500MB on desktop), and ffmpeg is terminated after each conversion to free memory.

The privacy model
This is the whole point:

100% client-side — files are processed in your browser
Zero network requests during compression
No accounts, no tracking, no analytics, no cookies
No server-side storage — there IS no server
Open source — read every line of code
Your files are yours. They shouldn't need to visit a server to be compressed.

Try it
Live: https://shrink-ray.vercel.app
Source: https://github.com/JeffreyHamilton6399/ShrinkRay

Feedback welcome — happy to answer questions about the ffmpeg.wasm setup, the 3D conversion pipeline, or the architecture.

Top comments (8)

Collapse
 
nazar-boyko profile image
Nazar Boyko

Rasterizing PDFs to images is the one step I'd put an asterisk on, especially next to your tax form example. The moment a text PDF becomes page images, you lose the selectable text and the search, and a document that's mostly text can even come out bigger than it went in. For the privacy crowd compressing tax forms, a searchable form that's now a stack of pictures might be a worse result than they expected, even if the file shrank. Have you thought about routing PDFs that are mostly text through something like mutool or qpdf, which recompress the streams and keep the text layer, and saving the rasterize path for PDFs that are already scans?

Collapse
 
jeffrey_hamilton6399 profile image
Jeffrey Hamilton

Really well-made point, Nazar. You're absolutely right — rasterizing a text-based PDF destroys the text layer, and for a tax form that defeats the purpose since people often need to search or copy from them.

The honest answer: I took the rasterization approach because it's what's possible with client-side JS libraries today. qpdf and mutool are native C tools — there's no WebAssembly port of them available yet. pdf-lib (which I'm using) can decompress and recompress streams, but it doesn't have the same stream-level optimization that qpdf does.

That said, I should be smarter about the routing. Right now every PDF goes through the rasterize path, which is wrong for text-heavy documents. What I should do:

Detect if the PDF is text-based or scan-based — check if pages have extractable text content via pdfjs
Text PDFs → skip rasterization, just recompress existing streams via pdf-lib (lossless, keeps text layer)
Scanned PDFs → rasterize (these are already images, so no text to lose)
The text detection is actually doable with pdfjs.getTextContent() — if a page returns text items, it's text-based. If it returns nothing, it's a scanned image.

I'll implement this routing in the next update. Thanks for catching this — the "tax form" example in my article is exactly the case where the current approach falls short. Appreciate the thoughtful feedback!

Collapse
 
jeffrey_hamilton6399 profile image
Jeffrey Hamilton

Also, since I have nothing better to do for the next week, I will implement we have talked about. Thanks for the feedback!

Collapse
 
voltagegpu profile image
VoltageGPU

Interesting approach using Web Audio and Canvas for compression without uploading files. I've worked on secure GPU-based processing for sensitive data, and client-side solutions like this are crucial for privacy. Just be careful with browser compatibility — some audio codecs can behave differently across platforms.

Collapse
 
jeffrey_hamilton6399 profile image
Jeffrey Hamilton

Great point on browser compatibility — that's been the trickiest part. Audio codecs especially vary wildly between Chrome, Safari, and Firefox.

For audio, I went with Web Audio API + lamejs specifically because decodeAudioData() uses the browser's native hardware-accelerated decoder, which is more consistent than relying on ffmpeg.wasm for every format. And for the Canvas image pipeline, createImageBitmap + OffscreenCanvas have solid cross-browser support now, with fallbacks for older browsers.

Collapse
 
frank_signorini profile image
Frank

How did you handle audio encoding with Web Audio, was it more efficient than using ffmpeg.wasm directly? I'd love to swap ideas on this, following for more content on wasm use cases.

Collapse
 
jeffrey_hamilton6399 profile image
Jeffrey Hamilton

Great question, Frank!

For audio, I went with Web Audio API + lamejs instead of ffmpeg.wasm for a few reasons:

Size — lamejs is ~15KB vs ffmpeg.wasm at 32MB. For audio-only use cases, loading ffmpeg just to re-encode an MP3 felt overkill.
Speed — Web Audio decodes natively via AudioContext.decodeAudioData(), which is hardware-accelerated. Then lamejs encodes the PCM data to MP3. The whole pipeline runs faster than ffmpeg's decode → encode cycle for audio.
Memory — ffmpeg holds 32MB in WASM memory the entire time. Web Audio + lamejs uses almost nothing by comparison, which matters a lot on mobile.
The tradeoff: lamejs only outputs MP3 (no Opus/OGG encoding), and it's a JS encoder so it's not as fast as a native one. But for the common case (compressing a podcast or song to 128k MP3), it's instant and keeps the bundle tiny.

I do fall back to ffmpeg.wasm for video audio extraction though — in that case ffmpeg is already loaded for the video encode, so it makes sense to use -vn -acodec copy for the audio track.

Would love to hear your approach too! What wasm use cases are you working on?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.