More
Сhoose
India

36, Defense Colony, 302012 Jaipur, Rajasthan

India

37, Udyog Vihar, 122015 Gurugram, Haryana

Running FFmpeg in the Browser: How We Built GoVid

Engineering
Running FFmpeg in the Browser: How We Built GoVid

Running FFmpeg in the Browser: How We Built GoVid

The first time GoVid worked end-to-end, I was compressing a 347MB MOV — a screen recording of a client walkthrough — and it came out at 22MB. The browser tab didn't freeze. The progress bar moved in real time. The file never left the machine. That result took longer to reach than we expected.

FFmpeg in WebAssembly sounds conceptually simple: compile FFmpeg to WASM, load it in the browser, call it on the file the user picks. The concept is correct. The path between concept and a tool that actually works — on real files, on real users' machines, without freezing the tab — is full of specific gotchas that the tutorials skip. This post covers what they are. GoVid supports MP4, MOV, AVI, MKV, and WebM, runs entirely in the browser with no server involved, and is the third client-side tool we've shipped this way — after our privacy-first architecture post and how we built GoPDF. Video was the hardest of the three.

Why Client-Side? Your Video Files Are Not for the Cloud

Video is the most sensitive file type most users ever hand to a tool. Screen recordings contain unreleased product demos, internal meetings, things said before the camera was noticed. Personal recordings contain faces, voices, location data. When you upload to a cloud compressor, you're trusting a startup you found via Google with material that, if leaked, could genuinely cause harm. We're not making a philosophical argument. We're making a practical one: client-side compression removes the need for trust entirely. We never receive the file, so there's nothing to mishandle.

The second reason is economics. Video files are large. Storage is cheap; bandwidth is not. Running server-side compression at meaningful scale means paying egress on every file. At 10,000 compressions per month — not an ambitious target for a free tool — that's a real line item. Client-side means the user's hardware handles the compute and the user's connection handles their own file. Our server load from GoVid is one static HTML file.

  • Zero infrastructure cost: the user's machine does the compute. We pay nothing per compression, at any volume.
  • No file size ceiling: server-side tools cap uploads at 500MB or 1GB. GoVid is limited only by what Chrome can hold in memory — we've compressed files up to 4GB in testing without hitting a hard wall.
  • No data retention risk: we never touch the file, so there's nothing to retain, leak, or be compelled to hand over.

FFmpeg.wasm — What It Is and What It Actually Costs You

FFmpeg.wasm is the FFmpeg binary compiled to WebAssembly by the ffmpegwasm project. You install @ffmpeg/ffmpeg and @ffmpeg/core, load the WASM binary, and issue FFmpeg commands the same way you would in a Linux terminal — except execution happens inside the browser's WASM runtime. The capability is completely real. So is the cost, and most tutorials don't mention it at all.

  • ~31MB WASM bundle on first load: @ffmpeg/core loads a ~31MB binary the first time a user hits GoVid. We cache it with a service worker, so repeat visits pay this cost once. But the first load is slow on a mobile connection. We added a loading indicator that honestly states "Loading FFmpeg (31MB) — this only happens once." Users who understand what's loading wait for it. Users who see a blank screen think the page is broken.
  • SharedArrayBuffer requires COOP and COEP headers: the multi-threaded version (@ffmpeg/core-mt) needs SharedArrayBuffer, which requires Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp on your server. These headers break Google Tag Manager, most analytics iframes, and any third-party script that uses cross-origin resources. We started with core-mt for better performance, then switched to core (single-threaded) when our analytics stopped reporting. Details on that in the next section.
  • Single-threaded means single-threaded: using @ffmpeg/core instead of core-mt means FFmpeg runs on one thread. A 200MB file that compresses in ~8 seconds with core-mt takes ~47 seconds with core. That is the trade-off we made for compatibility. We made it explicit in the UI with an estimated time display so users aren't left wondering if the tool is still running.

The UX Problem — Progress Without Freezing the Browser

The first internal version of GoVid ran FFmpeg directly on the main thread. The page froze completely for the entire duration of the compression. The progress bar didn't move. Chrome eventually showed "Page Unresponsive." Users had no idea if the tool was working or had crashed. The fix is obvious in hindsight: Web Workers. FFmpeg.wasm runs in a dedicated worker thread, separate from the main thread that controls the DOM. The worker sends progress updates to the main thread via postMessage. The main thread receives them and updates the UI without blocking.

// In the worker thread
ffmpeg.on('progress', ({ progress, time }) => {
  self.postMessage({ type: 'progress', progress, time });
});

// In the main thread
worker.onmessage = ({ data }) => {
  if (data.type === 'progress') {
    progressBar.style.width = `${data.progress * 100}%`;
    timeDisplay.textContent = `${Math.round(time / 1000000)}s processed`;
  }
};

One thing worth flagging: FFmpeg.wasm v0.12+ changed the API significantly from v0.11. The progress callback signature, the way you load the core binary, and the virtual file system API all changed. Most Stack Overflow answers and Medium tutorials are written for v0.11. We spent two days debugging before we checked the version pinned in the npm registry. Always pin your FFmpeg.wasm version and read the changelog when upgrading.

What We Got Wrong First

Main-thread execution was mistake one, covered above. Mistake two was trying to ship with @ffmpeg/core-mt in production. We needed the COOP and COEP headers to enable SharedArrayBuffer. Setting those headers on Netlify was straightforward via netlify.toml. What we didn't anticipate: those same headers caused our Netlify Forms submission confirmation — which uses an iframe — to silently fail. The form appeared to submit but the confirmation callback never fired. We spent three hours tracing it before we connected the COEP header to the broken iframe. We switched to @ffmpeg/core (single-threaded), dropped the headers, and the form worked again. The compression is slower. Everything else works.

Mistake three was memory handling on large files. GoVid reads the entire input file into browser memory using fetchFile(). A 2GB file requires roughly 2GB of available memory, plus working space for the output. Some users on lower-RAM machines hit out-of-memory errors that manifested as silent tab crashes. We added a warning for files over 1GB and a hard cap at 2GB with an explicit error message explaining the limitation. The crashes stopped.

Format Support and Real Compression Numbers

GoVid handles five input formats: MP4, MOV, AVI, MKV, and WebM. Output is H.264 MP4 by default, with optional WebM output. The compression preset we settled on after testing: -c:v libx264 -crf 28 -preset fast -c:a aac -b:a 128k. CRF 28 is visually lossless for most content at significantly reduced file sizes. We also offer CRF 23 ("high quality" mode) for users who need it — most choose the smaller file.

Numbers from testing on a mid-range laptop (Intel i5, 16GB RAM, Chrome 124):

  • 347MB MOV (screen recording, 1080p 30fps): compressed to 22MB in 47 seconds.
  • 189MB MP4 (4K footage, scaled to 1080p): compressed to 41MB in 61 seconds.
  • 78MB AVI (older camcorder export): compressed to 12MB in 29 seconds. AVI is where GoVid performs best — old AVI exports are massively bloated and compress extremely well.
  • 45MB WebM (already compressed with VP9): compressed to 38MB in 19 seconds. WebM-to-MP4 gains almost nothing in file size because VP9 is already efficient. We added a note in the UI warning users about this before they start.

Three Things to Take Away If You're Building This

First: use @ffmpeg/core (single-threaded), not core-mt, unless you've audited every third-party script and integration on your page for SharedArrayBuffer compatibility. The COOP and COEP headers break more than you expect — forms, analytics, chat widgets, payment SDKs. The performance trade-off is real but manageable if you set user expectations clearly in the UI.

Second: never run WASM on the main thread. Always offload to a Web Worker and use postMessage for progress updates back to the UI. This is not optional — on any file above a few MB, main-thread WASM will freeze the browser and make your tool unusable. Third: tell users about the 31MB bundle load upfront. Users who understand what's happening will wait. Users who see a blank spinner with no explanation will leave. You can try GoVid yourself at govid.flux8labs.com — free, no account required. If you're building something similar and want to talk through the architecture, reach out.


Frequently Asked Questions (FAQ)

  • Q1: Does GoVid upload my video to a server? No. GoVid runs entirely in your browser using WebAssembly. Your video file never leaves your machine — we have no server that receives, processes, or stores your files.

  • Q2: What video formats does GoVid support? GoVid supports MP4, MOV, AVI, MKV, and WebM as input. Output is H.264 MP4 by default, with optional WebM output. If you need a format not listed, adding support is a one-line change to the FFmpeg command — let us know.

  • Q3: Why is the first compression slow to start? The first run loads the FFmpeg WebAssembly binary (~31MB). After the first load, your browser caches it — subsequent compressions start immediately. We display a progress indicator during this initial load. On a slow connection, it may take 30–60 seconds.

Looking to make your mark? We'll help you turn
your project into a success story.

Ready to bring your ideas to life?
We're here to help