Intro – The Problem
Most online file converters work the same way: you upload your file, wait, then download the result. That raises privacy concerns and doubles transfer time for large files (upload + download = double the time).
Personal context: I'm an audio producer and needed to convert some legacy audio formats for a project. I tried 5 different converter sites but none of them supported the formats I needed - or hit me with endless pop-ups and CAPTCHA walls. The whole experience felt quite sketchy and so I thought: there has to be a better way. Turns out: there is – but it's not as simple as I thought.
I wondered: how much of this can realistically run in the browser?
Turns out: more than I expected, but with hard limits nobody talks about.
The LibreOffice WASM Dream (That Didn't Work)
My first idea: compile LibreOffice headless to WebAssembly. It's the Swiss Army knife for document conversion (DOCX, ODT, PDF, PPTX...). If I could get it running in the browser, I'd be done.
Reality check:
- Minimum binary size: ~150MB (even stripped)
- Startup time: 10-15 seconds just to initialize
- Memory usage: 200-300MB for a single DOCX→PDF conversion
- Browser memory ceiling: ~500MB before crashes
For converting one file? Not realistic today in a production browser environment.
What Actually Works in the Browser
Instead, I went format-by-format. Here's what I got working:
Video/Audio
FFmpeg.wasm (~20MB WASM binary)- Performance: ~10-20% of native FFmpeg
- Example: MP3 conversion takes ~10s (native: 1s)
- Trade-off: Slower, but files never leave device
Images
Sharp.wasm + image-js + canvg
- Performance: Image resize ~2s (native: 0.2s)
- Handles: PNG, JPEG, WebP, SVG→PNG, GIF
PDFs
pdf-lib (creation/manipulation)
pdf.js (rendering/text extraction)- Works great for simple operations (merge, split, text extraction)
- Fails on complex PDFs with embedded fonts/forms
Spreadsheets
SheetJS (xlsx.full.min.js)- Parses/creates Excel, CSV, ODS in browser
- Limitations: Large files (>10MB) freeze UI
Archives
JSZip + pako + tar.js
- ZIP/GZIP/TAR creation/extraction
- Fast enough for most use cases
Optimization strategy:
// Lazy load WASM modules on demand
const loadFFmpeg = async () => {
if (!ffmpegLoaded) {
await import('@ffmpeg/ffmpeg');
ffmpegLoaded = true;
}
};
// Total WASM payload: ~30MB
// Initial bundle: 150KB
The Performance Win I didn't expect
Here's a trick that saved me:
Problem: MP4→MOV conversion was timing out after >10 minutes. Full re-encoding with FFmpeg.wasm is just too slow.
The Solution: Container remux instead of re-encode.
// Check if codecs are compatible
const compatibleContainers = {
'mp4': ['mov', 'mkv'],
'mov': ['mp4', 'mkv'],
'mkv': ['mp4', 'mov']
};
if (areCompatibleContainers(from, to)) {
// Stream copy instead of re-encode
ffmpegOptions.push('-c', 'copy');
// Result: <10s instead of 10min (100× speedup)
}
This works because MP4 and MOV use the same codecs (H.264, AAC) – you're just changing the container wrapper.
The Hard Parts
1. Browser Memory Limits
Safari: ~500MB, then crash
Chrome: ~1GB, then slow-down
Firefox: ~800MB
Large video files? Forget it. One hits the ceiling fast.
2. Safari vs Chromium Hell
- Chrome:
SharedArrayBufferworks (multi-threading possible) - Safari:
SharedArrayBufferrequires COOP/COEP headers (breaks CDNs) - Solution: Detect and fallback to single-threaded mode
const canUseMultithreading =
(typeof SharedArrayBuffer !== 'undefined') &&
(window.crossOriginIsolated === true);
const threadCount = canUseMultithreading ? '4' : '1';
3. Large File Streaming
You can't load a 500MB video into memory. Browsers will kill the tab.
I tried: File streaming via ReadableStream
The reality: FFmpeg.wasm doesn't support streaming input yet (2025)
4. UI Freezing
Even with Web Workers, WASM blocks the main thread during initialization.
The solution: Show loading screen + delay 100ms to let UI render first.
The Hybrid Model (What I Actually Ended Up With. For Now.)
After weeks of experiments, here's what works:
Architecture split:
const shouldUseServer = (format, category, fileSize) => {
// Browser-side (90% of conversions)
if (category === 'image') return false;
if (category === 'audio') return false;
if (category === 'video' && fileSize < 50_000_000) return false;
// Server-side (10% of conversions)
if (format.includes('docx')) return true; // LibreOffice
if (format.includes('pdf') && complex) return true; // Poppler
if (fileSize > 100_000_000) return true; // Too large
return false;
};
Server-side stack:
- LibreOffice (headless) for Office docs
- Pandoc for Markdown/LaTeX/EPUB
- Native FFmpeg for large videos
- Poppler (pdftoppm, pdftotext) for complex PDFs
ClamAV malware scanning on all uploads (auto-reject infected files)- Auto-delete immediately / after 5 minutes
Why hybrid?
- Pure client-side = unrealistic for complex formats
- Pure server-side = privacy nightmare + infrastructure costs
- Hybrid = best UX + privacy trade-off
The Real Trade-offs
Client-Side Pros:
- Privacy (files never leave device)
- Zero infrastructure costs
- Instant conversion (no upload wait)
Client-Side Cons:
- 10-20% performance of native
- Memory limits (~50MB practical ceiling)
- Browser compatibility hell
- Some formats impossible (Office docs)
Server-Side Pros:
- Full performance (native tools)
- Unlimited file size
- Complex formats supported
Server-Side Cons:
- Privacy concerns (uploads)
- Infrastructure costs
- Upload/download overhead
Client
├── FFmpeg.wasm
├── Image tooling
├── PDF manipulation
└── SheetJS
↓ (fallback)
Server
├── LibreOffice
├── Pandoc
├── Native FFmpeg
└── Poppler
Key Takeaways
Pure browser-side is possible for ~90% of conversions (images, audio, simple video)
LibreOffice WASM is still unrealistic / unstable in 2026 (too large, too slow, too browser/platform-dependent)
Hybrid architecture is the pragmatic solution (browser-first, server fallback)
Container remux > re-encode when possible (100× speedup)
Memory limits are the real bottleneck, not CPU
If you're building something similar: start with WASM, fall back to server only when necessary, and be honest with users about what runs where.
What I Built
Based on these experiments, I ended up packaging this hybrid approach into a small project to test it in production.
Tech stack:
- Frontend: Next.js (static), FFmpeg.wasm, Sharp.wasm, pdf-lib, SheetJS
- Backend: Node/Express, LibreOffice, Pandoc, ClamAV, Redis
- Hosting: Cloudflare CDN (frontend), EU server (backend)
→ If you're curious: anythingconverter.com
Questions? I'm especially curious if anyone has cracked LibreOffice WASM or found better ways to handle large files in the browser.