ffmpeg.wasm is FFmpeg compiled to WebAssembly, letting you convert, compress, and extract audio from video files entirely in the browser — no server upload, no infrastructure cost, and no user data ever leaving the device. It works with any modern browser that supports WebAssembly.
In this article we'll go from installation to a working React/Next.js file converter component, covering the SharedArrayBuffer/COOP/COEP setup that trips up most developers, practical conversion examples, and the real-world performance limits you should know about.
Processing Video Without a Server
Traditional browser-based video conversion tools work like this:
- User uploads a video file to the server
- The server runs FFmpeg and converts the file
- The user downloads the converted result
This approach has two problems: it's slow (files transfer twice) and it costs money (server CPU). For large files, users can wait minutes just for the round trip.
ffmpeg.wasm is FFmpeg's C source compiled to WebAssembly via Emscripten. It runs inside the browser's JavaScript engine. All processing happens on the client — no file transfer to a server ever occurs.
Main advantages:
- No server infrastructure required (pure frontend)
- Files never leave the user's device — strong privacy guarantee
- Works offline after the WASM binary is cached
- Zero scaling cost
Realistic downsides:
- The WASM binary takes a few seconds to download on first load
- Processing speed is slower than native FFmpeg (often 5–20x slower)
- Large files can hit memory limits and fail
For small-to-medium files in practical conversion scenarios, the tradeoff is well worth it.
Setup and Basic Usage
Install the packages:
npm install @ffmpeg/ffmpeg @ffmpeg/util
Here is the basic usage pattern:
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util";
const ffmpeg = new FFmpeg();
async function convertVideo(inputFile: File): Promise<Blob> {
// Load the WASM binary (only on first call)
if (!ffmpeg.loaded) {
const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/umd";
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
wasmURL: await toBlobURL(
`${baseURL}/ffmpeg-core.wasm`,
"application/wasm"
),
});
}
// Write the input file into ffmpeg.wasm's virtual filesystem
await ffmpeg.writeFile("input.mp4", await fetchFile(inputFile));
// Run the FFmpeg command (MP4 → WebM conversion)
await ffmpeg.exec(["-i", "input.mp4", "-c:v", "libvpx-vp9", "output.webm"]);
// Read the output file back
const data = await ffmpeg.readFile("output.webm");
// Convert Uint8Array → Blob and return
return new Blob([data], { type: "video/webm" });
}
The array you pass to ffmpeg.exec() maps directly to FFmpeg command arguments after the ffmpeg binary name. ffmpeg -i input.mp4 output.webm becomes ["-i", "input.mp4", "output.webm"].
The SharedArrayBuffer Wall (COOP/COEP Headers)
To enable SharedArrayBuffer, your server must respond with two HTTP headers:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp
For Next.js, add this to next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async headers() {
return [
{
// Apply to all routes. Narrow the source path if you only
// need ffmpeg.wasm on specific pages.
source: "/(.*)",
headers: [
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
],
},
];
},
};
export default nextConfig;
Side effects to be aware of:
COOP/COEP restrict cross-origin resource loading. If your site loads external fonts, Google Analytics scripts, or other third-party assets, those will need crossorigin="anonymous" attributes or a Cross-Origin-Resource-Policy header on the resource server. Most major CDNs already send this header, but it can cause unexpected breakage if you haven't thought about it.
If you use the single-threaded build (@ffmpeg/core instead of @ffmpeg/core-mt), SharedArrayBuffer is not required and you can skip the COOP/COEP headers — at the cost of slower processing since multiple CPU cores won't be used. Starting with the single-threaded build is the easiest way to get up and running.
When deploying to Vercel, the next.config.ts headers config is respected automatically — no additional Vercel-specific configuration needed.
Video Conversion Examples
Here are practical conversion examples for common use cases.
MP4 → WebM (VP9, file size optimized):
await ffmpeg.exec([
"-i",
"input.mp4",
"-c:v",
"libvpx-vp9",
"-crf",
"33", // Quality (lower = higher quality, higher = smaller file)
"-b:v",
"0", // VBR mode (used together with -crf)
"-c:a",
"libopus",
"output.webm",
]);
MP4 → GIF (first 5 seconds):
await ffmpeg.exec([
"-i",
"input.mp4",
"-t",
"5", // Stop after 5 seconds
"-vf",
"fps=15,scale=480:-1:flags=lanczos", // Frame rate and resize
"-loop",
"0",
"output.gif",
]);
Extract audio from video (MP3):
await ffmpeg.exec([
"-i",
"input.mp4",
"-vn", // Discard video stream
"-c:a",
"libmp3lame",
"-q:a",
"2", // Quality (0-9, lower = higher quality)
"output.mp3",
]);
To track progress, register a listener with ffmpeg.on("progress", callback) before calling exec:
ffmpeg.on("progress", ({ progress, time }) => {
// progress: 0.0 to 1.0
// time: current position in the video being processed (microseconds)
console.log(`Progress: ${Math.round(progress * 100)}%`);
});
Building a React/Next.js Component
Here is a complete file converter component that users can drop a file into and receive a converted download.
"use client";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util";
import { useRef, useState } from "react";
const CORE_BASE_URL = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/umd";
export function VideoConverter() {
const ffmpegRef = useRef<FFmpeg>(new FFmpeg());
const [loaded, setLoaded] = useState(false);
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState<number | null>(null);
const [outputUrl, setOutputUrl] = useState<string | null>(null);
async function loadFFmpeg() {
if (loaded) return;
setLoading(true);
const ffmpeg = ffmpegRef.current;
await ffmpeg.load({
coreURL: await toBlobURL(
`${CORE_BASE_URL}/ffmpeg-core.js`,
"text/javascript"
),
wasmURL: await toBlobURL(
`${CORE_BASE_URL}/ffmpeg-core.wasm`,
"application/wasm"
),
});
ffmpeg.on("progress", ({ progress }) => {
setProgress(Math.round(progress * 100));
});
setLoaded(true);
setLoading(false);
}
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
await loadFFmpeg();
const ffmpeg = ffmpegRef.current;
setProgress(0);
setOutputUrl(null);
// Write the input file to the virtual filesystem
await ffmpeg.writeFile("input.mp4", await fetchFile(file));
// Run conversion
await ffmpeg.exec(["-i", "input.mp4", "-c:v", "libvpx-vp9", "-crf", "33", "-b:v", "0", "output.webm"]);
// Read output and create a download URL
const data = await ffmpeg.readFile("output.webm");
const blob = new Blob([data as Uint8Array], { type: "video/webm" });
const url = URL.createObjectURL(blob);
setOutputUrl(url);
setProgress(null);
}
return (
<div className="space-y-4 p-6 border rounded-lg">
<h2 className="text-xl font-bold">Video Converter (MP4 → WebM)</h2>
<input
type="file"
accept="video/mp4"
onChange={handleFileChange}
disabled={loading || progress !== null}
className="block"
/>
{loading && (
<p className="text-sm text-muted-foreground">Loading FFmpeg...</p>
)}
{progress !== null && (
<div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-sm mt-1">{progress}%</p>
</div>
)}
{outputUrl && (
<div className="space-y-2">
<video src={outputUrl} controls className="w-full max-w-lg" />
<a
href={outputUrl}
download="output.webm"
className="inline-block px-4 py-2 bg-primary text-primary-foreground rounded"
>
Download
</a>
</div>
)}
</div>
);
}
A few things worth noting about this implementation. The FFmpeg instance lives in a useRef — not useState — because you don't want React to recreate it on every render. Loading the WASM binary is expensive, so you want to do it once and reuse the same instance.
The URL.createObjectURL call creates a Blob URL that should be revoked when it's no longer needed. For production code, call URL.revokeObjectURL(url) in a useEffect cleanup function to avoid memory leaks.
Limitations and Performance
Processing speed:
ffmpeg.wasm is noticeably slower than native FFmpeg. A rough benchmark: converting a 5-minute MP4 to WebM that takes 30 seconds natively might take 3–10 minutes in the browser, depending on device CPU. This is an inherent cost of the WASM sandbox.
Codec availability:
The default ffmpeg.wasm build includes a limited codec set. Codecs with patent complications — like H.265 (HEVC) encoding or AV1 encoding — may not be available in the standard build. Common use cases (H.264 decode, VP9 encode, format conversion) work fine.
Browser compatibility:
Any modern browser supporting WebAssembly and SharedArrayBuffer will work: Chrome 92+, Firefox 79+, Safari 15.2+. Check the Can I Use SharedArrayBuffer table for the latest compatibility data. IE is unsupported, but in 2026 that's rarely a concern.
Threading model:
ffmpeg.wasm uses Web Workers internally, so conversions don't block the main thread — your UI stays responsive during processing. The progress callback fires from within the Worker, so you can drive a progress bar without any extra threading logic on your side.
FAQ
Is ffmpeg.wasm free to use in commercial projects?
Yes. ffmpeg.wasm itself is MIT licensed. However, the underlying FFmpeg libraries carry LGPL/GPL licenses depending on which codecs are enabled. For commercial use, check the FFmpeg license details to understand your obligations.
How large is the ffmpeg.wasm download?
The core WASM binary is approximately 25–30 MB. It's downloaded once and can be cached by the browser's service worker or HTTP cache. On subsequent visits, loading is near-instant.
Can I use ffmpeg.wasm with Vue, Svelte, or plain JavaScript?
Absolutely. The React component in this article is just one example. ffmpeg.wasm is a plain JavaScript library — it works with any framework or no framework at all. The core API (new FFmpeg(), load(), exec(), readFile(), writeFile()) is framework-agnostic.
Does ffmpeg.wasm support H.265 (HEVC) encoding?
The default build does not include H.265 encoding due to patent licensing concerns. H.264 decoding and VP9/VP8 encoding are available. If you need H.265, you would need to compile a custom WASM build with the appropriate codec flags enabled — which also means taking on the licensing responsibility.
Can I process multiple files at the same time?
A single FFmpeg instance processes one file at a time. To handle multiple files, you can either queue them sequentially or create multiple FFmpeg instances — though keep in mind that each instance consumes its own memory for the virtual filesystem. For batch processing workflows on the server side, see Automate Video Processing with FFmpeg and Python.
Why is ffmpeg.wasm so much slower than native FFmpeg?
WebAssembly runs in a sandboxed environment without direct access to hardware acceleration. Native FFmpeg can use GPU encoding (NVENC/QSV) and SIMD instructions that WASM can't fully leverage. Expect 5–20x slower processing compared to native, depending on the codec and device.
Do I need COOP/COEP headers for the single-threaded build?
No. The single-threaded build (@ffmpeg/core) does not use SharedArrayBuffer and works without Cross-Origin Isolation headers. It's slower since it can't use multiple CPU cores, but it's the easiest way to get started without touching server configuration.
Can ffmpeg.wasm work offline?
Yes, once the WASM binary is cached (either through the browser's HTTP cache or a service worker), ffmpeg.wasm works completely offline. All processing happens locally — no network requests are made during conversion.
Wrapping Up
ffmpeg.wasm opens the door to serverless video processing tools built entirely on the frontend.
Key takeaways:
- Use v0.12+ API: Import the
FFmpegclass and instantiate withnew FFmpeg()— not the oldcreateFFmpeg()factory - Set COOP/COEP headers: Cross-Origin Isolation is required for SharedArrayBuffer; wire it up in
next.config.tsbefore you do anything else - Guard against large files: Files over 1 GB can exhaust browser memory — add a size check in your UI
- Keep the FFmpeg instance in a ref: Use
useRefto hold it across renders and avoid paying the WASM load cost more than once - Set expectations on speed: Processing is slower than native; a progress bar makes the wait bearable
For small-to-medium file conversion, GIF generation, and audio extraction, ffmpeg.wasm delivers practical performance. If you're building a privacy-first tool where sending files to a server is a non-starter, or a service where you want zero infrastructure cost per conversion, ffmpeg.wasm is one of the most interesting tools in the frontend ecosystem right now.
Related articles:
- FFmpeg Commands: A Practical Guide from Basics to Advanced
- The Complete Guide to Video Compression with FFmpeg
- Stream HLS Video with FFmpeg and a CDN
- FFmpeg Too Hard? GUI Alternatives Compared
- Automate Video Processing with FFmpeg and Python
- FFmpeg GPU Encoding: NVENC and QSV Guide
- FFmpeg Commercial License Guide