ClearRec
EngineeringWeb PlatformVideo

The complete guide to ffmpeg.wasm in 2026: browser-based video encoding without a server

How ffmpeg.wasm works in 2026 — the WebAssembly build of ffmpeg that runs inside a browser tab, what it can and can't do, real benchmarks, gotchas with SharedArrayBuffer, and how ClearRec uses it to trim and re-encode screen recordings client-side.

M. H. Tawfik19 min read

The first time you watch ffmpeg run inside a browser tab — same binary that's compiled into every Linux server, same command-line flags, same output — you assume something must have been stripped to make it fit. It hasn't been. ffmpeg.wasm is the actual ffmpeg C codebase compiled to WebAssembly through Emscripten, the entire 10-million-line libav* stack, packaged into a JavaScript module you can <script>-tag onto a page. In ClearRec it's how every trim, crop, and GIF export happens — the same encoder a YouTube ingest server uses runs inside the Chrome tab on your laptop. This post is the 2026-current picture of what works, what doesn't, what's fast, what's slow, and what to reach for it for.

The 30-second version

  • ffmpeg.wasm = ffmpeg compiled to WebAssembly + a thin JS wrapper. ~30 MB gzipped on the wire (the multi-threaded MT build), ~80 MB unpacked in memory.
  • It runs inside a Web Worker. No main-thread freezing during a 10-second encode.
  • It needs SharedArrayBuffer for multi-threading, which means your page needs Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers set correctly. Without them, you fall back to single-threaded mode, which is ~3-4× slower.
  • Real encode speeds on a 2024 M3 laptop: ~1.8× real-time for H.264 at 1080p / 30 fps, ~0.9× for VP9, ~0.3× for AV1.
  • Real-world use cases that work today: trim, crop, transcode between H.264/VP9/AV1, GIF export with palettegen, audio extraction, format remuxing, frame extraction, basic filters (scale, fps, fade).
  • Use cases that don't work yet: hardware-accelerated encoding (CPU only), GPU shader filters, anything that needs codecs from the proprietary side of ffmpeg's source tree.

If you ship a tool that touches video — recorders, editors, format converters, ad-tech video processors, learning platforms — ffmpeg.wasm is genuinely the answer in 2026 for everything that doesn't need hardware encode. The rest of this post is the why behind each of those bullets.

What ffmpeg.wasm actually is

Stock ffmpeg is a C program. Emscripten is a toolchain (from the LLVM project) that compiles C to WebAssembly — a low-level, near-native instruction set that browsers can execute inside a sandboxed runtime. Take the ffmpeg source, run it through Emscripten with the right flags, and you get an .wasm module the browser can load and execute alongside JavaScript.

The shipping ffmpeg.wasm distribution is two parts:

  1. ffmpeg-core.wasm — the compiled binary. About 30 MB gzipped (multi-thread build), 50 MB unpacked. This is the "ffmpeg executable" equivalent.
  2. @ffmpeg/ffmpeg — a thin JavaScript wrapper that loads the wasm, sets up a virtual filesystem in memory, marshals JS strings into the wasm heap, runs the binary, and reads results back out.

You use it like this:

import { FFmpeg } from "@ffmpeg/ffmpeg";

const ffmpeg = new FFmpeg();
await ffmpeg.load();                 // pulls down ffmpeg-core.wasm, mounts the FS
await ffmpeg.writeFile("in.mp4", inputBytes);
await ffmpeg.exec(["-i", "in.mp4", "-t", "10", "-c", "copy", "out.mp4"]);
const out = await ffmpeg.readFile("out.mp4");

Five lines of JavaScript to trim a video to its first ten seconds, with no server round-trip and no upload. The first await ffmpeg.load() is the only "expensive" call — it downloads and instantiates the wasm module, which takes ~4 seconds on a warm cache and ~10 seconds on a cold cache. Every subsequent exec() runs in the already-loaded instance.

Why it matters for screen recording

The default architecture for "MP4 to GIF" on the web in 2026 is still: upload your file to someone's server, wait for them to convert it, download the result. This is an appalling trade for screen recordings, where the file you're sending often contains:

  • Customer support sessions (with PII)
  • Internal admin dashboards (with credentials)
  • Security demos (with attack surface visible)
  • Healthcare or legal interfaces (with regulatory exposure)
  • A pre-release product (with NDA exposure)

ffmpeg.wasm collapses that flow into a single client-side step. The bytes of the recording never leave the browser tab. ClearRec uses this for every editor operation: trim, crop, resolution scaling, frame-rate dropping, GIF re-encode with palettegen, format conversion. The same ffmpeg-core.wasm binary handles every one of those — same flags you'd type at a Linux command line, executed by a wasm runtime in the Chrome tab where the recording was made.

The privacy story is genuine in a way that "we encrypt at rest" never quite is: the recording is in memory, the encoder is in memory, the output is in memory, and the only thing that leaves the browser is the file you save to your Downloads folder.

SharedArrayBuffer, COOP, COEP — the part everyone trips over

The single most common "why is ffmpeg.wasm so slow?" complaint reduces to one thing: multi-threading isn't enabled because the page didn't set the right HTTP headers.

ffmpeg can use multiple CPU cores when encoding. Inside a browser, the way it does that is via SharedArrayBuffer — a shared-memory primitive that lets a Web Worker and the main thread address the same chunk of memory. After the Spectre/Meltdown disclosures in 2018, browsers gated SharedArrayBuffer behind a cross-origin isolation policy: your page can only use it if it sets these two response headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Without those, SharedArrayBuffer is undefined and ffmpeg.wasm silently falls back to single-threaded mode. On a typical 1-minute 1080p H.264 encode, that's the difference between a 35-second wait and a 110-second wait.

A few important practical notes:

  • Chrome Extensions don't need the headers — extension contexts have cross-origin isolation by default since Manifest V3. This is part of why ClearRec runs ffmpeg.wasm inside an extension page rather than a regular web page; we get multi-threading for free.
  • The headers also restrict what your page can embed. Once you set COEP: require-corp, every iframe / image / script you load from a third party needs to send Cross-Origin-Resource-Policy: cross-origin or it fails. Plan for it.
  • crossOriginIsolated is the JS check. If window.crossOriginIsolated === true, you're good. If it's false, your headers are missing or wrong.
  • Service workers can apply the headers if you can't change your origin's headers directly (some static hosts). There's a community pattern for a coi-serviceworker shim that injects the headers locally.

If you're building anything ffmpeg.wasm-adjacent, get this right first. The performance gap between single-threaded and multi-threaded modes is dramatic enough that everything else (codec choice, bitrate, filter chain) is rounding-error in comparison.

What ffmpeg.wasm can do — the feature surface

The build that ships at @ffmpeg/core includes most of the codecs and filters that matter for general video work. From our own production use:

Containers (read and write): MP4, WebM, MKV, MOV, AVI, FLV, OGG, GIF, raw.

Video codecs (decode and encode): H.264 (libx264), H.265 (libx265, slow), VP8, VP9 (libvpx), AV1 (libaom — slow), MPEG-4, MJPEG, prores.

Audio codecs (decode and encode): AAC, MP3, Opus, Vorbis, FLAC, PCM, AC3.

Filters that matter for screen recording: scale, fps, crop, pad, fade, drawtext, overlay, palettegen, paletteuse, format, vflip, hflip, transpose, setpts (speed change), atempo (audio speed).

Frame extraction: -vf "select=eq(n,N)" to pull a specific frame, or -vf "fps=1" to extract one frame per second.

Concat: the concat filter and demuxer both work for stitching clips together.

What's not in the default build:

  • Hardware encoders (NVENC, QuickSync, VideoToolbox). All encoding is software-only — the wasm runtime has no access to the GPU.
  • Patent-encumbered codecs that some Linux distros strip (e.g., MP3 encoding without LAME).
  • Niche broadcast filters (xfade, certain colorspace conversions).
  • The full deshake / stabilization pipeline (works but is impractically slow in wasm).

You can extend the build yourself by recompiling ffmpeg with Emscripten and the codec library of your choice — the project documents how. For 95% of cases, the default build is enough.

Real-world benchmarks (2026, M3 Pro MacBook)

We ran the same set of operations against a 60-second 1080p screen recording on a stock 2024 MacBook Pro M3 Pro (12-core), Chrome 142, ffmpeg.wasm 0.13 multi-thread build, page cross-origin-isolated.

OperationWall-clock timeRealtime factorNotes
Trim (no re-encode, -c copy)0.4 s150× rtPure container repack, very fast.
H.264 re-encode (libx264, preset=fast, CRF 23)33 s1.8× rtThe default ClearRec MP4 export.
VP9 re-encode (libvpx-vp9, CQ 32)66 s0.9× rtThe default ClearRec WebM export.
AV1 re-encode (libaom-av1, cpu-used 8)195 s0.3× rtWhy we don't make this the default.
GIF (palettegen + paletteuse, 15 fps)22 s2.7× rtTwo-pass; first pass is fast.
Frame extract (1 fps as PNG)4 s15× rtI/O-bound, not CPU.
Audio extract (AAC remux, no re-encode)0.3 s200× rtContainer repack only.
Crop + scale to 720p + H.264 re-encode28 s2.1× rtFaster than full-res because fewer pixels.

A few things worth pointing out:

  • Trim without re-encode is essentially free. When you just need to cut a clip down to its first 30 seconds and you don't care about precision past the nearest keyframe (~1-2 second granularity), -c copy does it instantly. This is what ClearRec's "fast cut" path uses when the user's trim boundaries happen to land on keyframes.
  • The cost gap between H.264, VP9, and AV1 is real and increasing. AV1 produces files ~20% smaller than VP9, but at ~5× the encode wall-clock. Until AV1 hardware encode is universal (we're still a few years away), it's not the right default for "fast export from the browser".
  • GIF is faster than people expect. The palettegen pass is cheap; the paletteuse pass scales with the number of frames. A 6-second GIF at 15 fps (90 frames) completes in about 4 seconds on this hardware.
  • Single-threaded mode (no SharedArrayBuffer) triples every one of these numbers. We've seen pages take 110+ seconds to do what should be a 35-second job because the COOP/COEP headers weren't set.

Memory: how much, where, and the 2 GB cliff

ffmpeg.wasm runs inside the wasm linear memory, which is capped at 4 GB on 64-bit hosts. In practice, the practical cap is closer to 2 GB because the wasm runtime needs headroom and some operations make multiple copies of intermediate frames.

For a 1080p 60-second clip at 5 Mbps, peak memory looks like:

  • Input file in the virtual FS: ~38 MB
  • Decoder ring buffer: ~50 MB
  • Encoder ring buffer: ~50 MB
  • Filter intermediate frames: ~100 MB
  • Output file: ~38 MB
  • ffmpeg internals: ~80 MB

Roughly 350 MB peak for a 1-minute clip. Scales linearly with both duration and resolution.

The cliff to watch for: a 60-minute 1440p / 60 fps recording at 20 Mbps is roughly 9 GB of raw video data uncompressed. ffmpeg streams through it (it doesn't load the whole file into RAM) but the intermediate buffers + the input file copy in the virtual FS can push the wasm heap close to its limit. ClearRec handles this by processing long recordings in chunks rather than as a single ffmpeg invocation — splitting at keyframes, encoding each chunk, then concatenating with -c copy.

If you're building anything that handles long-form video in ffmpeg.wasm, this is the architectural problem you'll trip over second (after COOP/COEP). The mitigation is the same as on a server: stream, chunk, concat.

The virtual filesystem: how files actually move in and out

A common confusion: ffmpeg.wasm doesn't read from your laptop's filesystem. It can't — wasm sandboxes file access entirely. Instead, it ships with a virtual in-memory filesystem (MEMFS) that you populate before invoking exec().

// Move the input bytes into the virtual FS
await ffmpeg.writeFile("in.mp4", new Uint8Array(inputArrayBuffer));

// ffmpeg now sees "in.mp4" at the root of its filesystem
await ffmpeg.exec(["-i", "in.mp4", "-vf", "fps=15", "out.gif"]);

// Read the output back out of the virtual FS
const outputBytes = await ffmpeg.readFile("out.gif");

Two performance implications:

  1. The writeFile and readFile calls are real data copies. A 100 MB input gets copied from a Blob into the wasm heap. On modern hardware that's under 100 ms but it's not zero.
  2. The virtual filesystem persists across exec() calls inside the same FFmpeg instance. If you trim then re-encode, you don't need to write the same input twice — leave it in MEMFS, run a second exec() with a new output path.

There's also a WORKERFS mode that exposes a real File object directly to ffmpeg without copying — useful if your input is enormous and you want to skip the initial write. It's slower per-read because every disk-like read crosses the JS/wasm boundary, but the memory savings can be worth it for clips bigger than ~500 MB.

Compared with the alternatives

Three other "browser-side video" options exist in 2026, and they all have a place:

MediaRecorder API (built into the browser)

What it does: takes a MediaStream (from getDisplayMedia() or getUserMedia()) and encodes it to WebM-VP9 or MP4-H.264 in real time as you record. Hardware-accelerated where the device supports it.

When to use it: at the moment of capture. ClearRec uses MediaRecorder to record the original screen capture — ffmpeg.wasm only enters the picture after the recording is done, for trim/crop/transcode.

When not to use it: post-processing. MediaRecorder can't trim an existing file, can't transcode between formats, can't apply filters. It's a capture API only.

WebCodecs API (also built into the browser)

What it does: low-level, frame-by-frame encode and decode using the browser's built-in codec stack. Hardware-accelerated on supporting devices. Available in all evergreen browsers since 2023.

When to use it: when you need per-frame control. WebCodecs is the right primitive if you're writing a real timeline editor with frame-accurate ins and outs and effects on individual frames. The tradeoff is that you have to assemble the container yourself — WebCodecs gives you EncodedVideoChunk objects, not an MP4 file. You either pipe them into a JavaScript MP4 muxer (mp4-muxer, mp4box.js) or back into ffmpeg.wasm's remuxer.

When not to use it: when you want a one-shot "convert this to that" operation. WebCodecs is a primitive; ffmpeg.wasm is a complete tool.

A server

What it does: the entire backend that AWS Elemental MediaConvert, Mux, and a hundred small startups are built around.

When to use it: when the video can leave the user's device. Server-side transcoding is faster (hardware encoders on dedicated boxes, parallelism across multiple machines), supports more codecs, handles longer files, and produces smaller files at higher quality because you can throw 20× the CPU at it.

When not to use it: when the video can't leave the user's device. Screen recordings, healthcare imagery, internal corporate UI, security-sensitive material, anything under a data-residency policy. This is the bucket ClearRec lives in.

A useful mental model: ffmpeg.wasm is for the "I want server-grade processing without a server" use cases. It's never going to be as fast as a dedicated transcode box, but it doesn't need to be — it just needs to be fast enough that the user doesn't notice, and private enough that the data stays on the device.

The build process, briefly

If you want to extend ffmpeg.wasm — add a codec the default build doesn't include, strip down for smaller bundle size, enable an obscure filter — the path is:

  1. Clone ffmpegwasm/ffmpeg.wasm repo.
  2. Make sure you have Emscripten (emsdk install latest && emsdk activate latest).
  3. Edit build/configure-ffmpeg.sh to add/remove --enable-* and --disable-* flags.
  4. Run ./build/build.sh.
  5. Wait ~25 minutes on a fast laptop.

The build output goes in dist/. You can publish it as your own npm package or vendor it into your project directly.

ClearRec ships the stock multi-thread build; we've evaluated stripping it for smaller download size but the trade-off (saving ~8 MB at the cost of dropping support for some output formats) isn't worth the developer-experience hit for users who occasionally need WebM or AV1.

Common gotchas

A handful of issues we've hit ourselves and seen others hit:

"It works in dev but not in production." Almost always COOP/COEP headers. Dev servers like Vite ship them by default; static hosts often don't. Check crossOriginIsolated in the production page.

"fetch() of the ffmpeg-core.wasm file 404s." The wasm core ships separately from the @ffmpeg/ffmpeg npm package and has to be self-hosted (or pulled from a CDN). Set coreURL and wasmURL explicitly when you call ffmpeg.load().

"It's stuck at 99% and never finishes." Usually a malformed input file that confuses the demuxer. Try with -err_detect ignore_err to skip past glitchy frames, or remux to MP4 first with -c copy and run the real operation on the remuxed file.

**"writeFile throws RangeError: Maximum call stack size exceeded."** Happens when you pass a Uint8Arrayover ~100 MB. Chunk the write, or pass aFileand useWORKERFS` mode.

"The encode is fine but the audio is missing." A common ffmpeg gotcha unrelated to wasm: if you specify -c:v libx264 you've implicitly asked for video-only output. Add -c:a aac -b:a 128k (or -c:a copy if the input audio codec is already what you want).

"AV1 encoder is hanging." It's not hung; it's just slow. libaom-av1 at default settings can take 3-5× the clip's wall-clock to encode. Use cpu-used=8 (faster, slightly larger files) for interactive use.

"The page freezes during the encode." ffmpeg.wasm runs inside a Worker by default in recent versions, so this shouldn't happen. If it does, you're either using an older build or running on the main thread explicitly. Move it to a Worker.

Frequently asked questions

Q: Is ffmpeg.wasm production-ready in 2026? Yes. The library is at v0.13, the API is stable, several open-source projects (including ClearRec) ship it in production, and the maintainer is responsive. The main risk profile is around browser changes to SharedArrayBuffer policy, which has been stable for ~4 years now.

Q: How big is the download for end users? ~30 MB gzipped for the multi-thread build, ~12 MB for the single-thread build. The wasm is cached by the browser after first load, so subsequent visits are instant.

Q: Does it work on mobile? Yes, but slower. A 2024 iPhone 16 Pro runs ffmpeg.wasm at roughly half the throughput of an M3 MacBook Pro. Memory limits are also tighter — Safari on iOS caps wasm memory at ~2 GB. For short clips (under 60 seconds), it's fine; for long-form, plan on chunking.

Q: What's the legal status? Can I ship this in a commercial product? ffmpeg itself is LGPL (with a GPL build path if you enable GPL-only codecs). ffmpeg.wasm inherits that license. LGPL is generally fine for commercial use — you just need to allow users to replace the ffmpeg.wasm binary, which is trivial in a web context (it's a downloadable file). For belt-and-braces, ship the ffmpeg.wasm source alongside your build or link to the upstream repo.

Q: Is it sandboxed? Can ffmpeg.wasm read my filesystem? No. Wasm cannot access the host filesystem, the network, or any device. Everything ffmpeg sees comes from the in-memory virtual filesystem you populate via writeFile(). The sandbox is enforced by the browser, the same way it enforces it for every other wasm module.

Q: Why doesn't ClearRec just use a server? Because the bytes of a screen recording can be sensitive in ways that aren't always obvious. Admin dashboards, customer support sessions, healthcare interfaces, internal CRMs — these are the bulk of what people record. A "free MP4-to-GIF converter" website that asks you to upload one of those is exactly the kind of friction we wanted to remove. Doing the work in a wasm-sandboxed tab means the bytes never leave the laptop. The privacy page has the full data-flow breakdown.

Q: Can I see ClearRec's ffmpeg.wasm code? Yes — the repo is published. Search for ffmpeg.exec( and you'll see every command we invoke. There are no fetch( or XMLHttpRequest calls in the editor module; you can grep for it.

Q: What's next for ffmpeg.wasm? The project's roadmap has been around WebGPU acceleration for some filters, smaller core builds (sub-15 MB), and better thread scheduling. None of those are shipping today; all are realistic for 2026-2027.

When to reach for ffmpeg.wasm vs not

A decision rule:

  • Want to trim, transcode, or convert a video in the browser, without a server? ffmpeg.wasm.
  • Want frame-accurate per-frame manipulation with a custom timeline? WebCodecs (and use ffmpeg.wasm only for the final mux).
  • Want to capture a screen or webcam stream and save it? MediaRecorder, not ffmpeg.wasm. (Then ffmpeg.wasm for post-processing.)
  • Doing batch transcode at scale, video can leave the device? Server. Don't fight gravity.
  • Building a Chrome extension that touches video? ffmpeg.wasm, and you get the COOP/COEP headers for free.

The summary

ffmpeg.wasm in 2026 is the right answer for any client-side video pipeline that needs more than what MediaRecorder gives you and where the video shouldn't leave the device. It's stable, it's fast enough on modern hardware, and it ships the same encoder Linux servers do. The two things to get right are cross-origin-isolation headers (without which it's 3× slower) and memory chunking for anything longer than a few minutes.

If you want to see it running in production, install ClearRec from the Chrome Web Store. Every trim, crop, GIF export, and resolution scaling in the editor goes through ffmpeg.wasm, locally, with no upload. The MP4 in your Downloads folder was encoded by the same binary every Linux server in the world uses — it just happened to run in a Chrome tab.

See also