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
- 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.
- 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.
- 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.
- 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.
- 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)
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?
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!
Also, since I have nothing better to do for the next week, I will implement we have talked about. Thanks for the feedback!
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.
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.
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.
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.