2026-09-08
Why video conversion in a browser takes so long (and which operations are actually instant)
You drop a clip into a browser-based converter, press the button, and then sit there for noticeably longer than you expected. Nothing is broken. The wait is the invoice for the thing that made you pick the tool in the first place: nothing was uploaded, so nothing was converted on somebody else's hardware. Your own laptop is the render farm now, and it is doing the job with one hand tied behind its back.
This is the itemised version of that invoice — where the seconds actually go, which operations cost almost nothing, and where the wall is, because there is a wall and it is a hard stop rather than a long wait. The numbers here are our own measurements from NearVid's engine work, and the caveats around them are printed in full rather than buried.
Before frame one: the engine has to arrive
A server-side converter has FFmpeg already installed. A browser-based one has to ship it to you.
NearVid's video engine is a self-built, GPL-free FFmpeg compiled to WebAssembly, and the compiled
core is a single 25MB .wasm file that the browser must download, compile and
instantiate before it can look at your video at all.
The good news is that this is mostly a first-visit cost — the file is served from one stable URL and cached like any other static asset, so a second conversion later the same day skips the download entirely. The instantiation cost that remains is small: in our own Playwright runs against real Chromium, loading the core landed between 563ms and 702ms across our runs once the files were local (597ms for the single-thread build, 702ms for the multi-thread one in the performance run; 563ms for the multi-thread GPL-free core we actually ship, measured separately). NearVid also starts that load in the background the moment you pick a file, before you have chosen what to do with it, precisely so the wait overlaps with you reading the option cards instead of following them.
So the engine download is a real cost, but it is not the cost you feel on a slow conversion. That one is further down.
It is FFmpeg, with the assembly taken out
Here is the part people usually skip past with a vague "WebAssembly is slower." It is more specific than that, and the specifics are readable in the build recipe.
FFmpeg gets a lot of its speed from hand-written, per-CPU assembly in its hot loops. A WebAssembly
build cannot use any of it, and the
ffmpeg.wasm build scripts our core is
derived from say so outright: FFmpeg's own configure is invoked with
--disable-asm, and so are several of the bundled libraries it links against. What
replaces that assembly is -msimd128 — WebAssembly SIMD. That is a real vector
instruction set rather than a consolation prize, but it is a deliberately conservative one:
V8's writeup notes the proposal is "limited to
standardizing Fixed-Width 128-bit SIMD operations," chosen so the specification "guarantees
portable performance," and MDN documents the resulting
v128
value type as exactly 128 bits of packed data.
The tradeoff is in the word portable. One fixed 128-bit width, chosen so the same binary performs predictably everywhere, instead of the widest vectors your particular processor happens to implement plus years of codec-specific assembly tuned for it. For a video encoder — which is mostly motion estimation, and motion estimation is mostly the same few arithmetic loops run several million times — that gap is where your seconds go.
Threads exist, but two HTTP headers decide whether you get them
Multi-threading is the single biggest lever available, and it is not a lever the page can simply
pull. Emscripten implements the pthreads API on top of
SharedArrayBuffer, and
SharedArrayBuffer has been gated behind cross-origin isolation ever since it was re-enabled after
Spectre. MDN
puts the requirement plainly: "To use shared memory your document must be in a secure context and
cross-origin isolated." In practice that means the server has to send
Cross-Origin-Opener-Policy: same-origin
and Cross-Origin-Embedder-Policy: require-corp on that specific page.
This is not a soft degrade. In our measurements, loading the multi-thread core on a page without
those headers did not quietly fall back to one thread — it threw
ReferenceError: SharedArrayBuffer is not defined after 29ms and stopped. It works or it
does not. (This is also why NearVid's processing screen lives on its own separate route: those
headers block third-party embeds outright, so they can only be applied to a page that has none.)
When the headers are in place, the payoff is real but bounded. Same clip, same browser, same machine, the only difference being cross-origin isolation: 13,426ms single-threaded versus 6,940ms multi-threaded — about 1.93x. Roughly a halving on a four-core machine, not a tenfold win.
What each operation actually costs
These are measured, not estimated. The clip was a synthetic 640x360, 30fps, 8-second test video,
run in real Chromium on a sandbox cloud VM reporting hardwareConcurrency: 4:
- Trim (stream copy): 32ms multi-threaded, 56ms single-threaded. Effectively instant.
- Extract a still frame: decodes to one frame and writes an image. In the same family as trim, not as re-encoding.
- Convert to GIF, 8 seconds at reduced size: 158ms. Cheap because a GIF is small in pixels, even though the resulting file is not small in bytes.
- Re-encode to WebM: 6,940ms multi-threaded, 13,426ms single-threaded. For eight seconds of 640x360. That is roughly real time on four cores, and slower than real time on one.
Now the caveats, in full, because a benchmark quoted without them is just a number. This was a four-core cloud VM, not a phone. Low-end mobile hardware is slower, and our own engine notes say explicitly that the phone-versus-cloud-vCPU gap was never measured here, so we are not going to put a multiplier on it. The clip was 640x360; encoding cost scales roughly with pixel count, so 1080p is several times as many pixels per frame as this test and 4K several times again. And a synthetic test pattern is not real footage — a static talking head and a handheld pan through foliage give a motion estimator very different amounts of work.
The shape of the result is what generalises, not the exact milliseconds: copying is roughly two hundred times cheaper than re-encoding, and that ratio is not an artefact of running in a browser. It is why the single most useful habit is picking the cheapest operation that solves your actual problem.
Memory is the wall, and it is a hard one
Slowness you can wait out. The memory ceiling you cannot, so it is worth knowing where it is.
NearVid reads your file fully into memory and then hands a copy into the WebAssembly heap, so a file briefly exists more than once in RAM. On top of that, the multi-threaded ffmpeg.wasm build requests a fixed 1024MB heap at startup and does not enable memory growth — the upstream build script says why in a comment: growth "is not recommended when using threads, thus we use a large initial memory." A single-threaded build grows on demand; the fast one does not. And above all of that sits the platform limit: 4GB is the ceiling for today's 32-bit WebAssembly, because that is what 32-bit pointers can address.
This is why NearVid states a file-size limit and enforces it before anything starts, rather than letting you watch a progress bar for four minutes and then crash: 500MB per file on a desktop, 50MB on phones and on devices reporting 4GB of RAM or less. Over the limit, the file is refused immediately. That is a deliberately conservative number, not the theoretical maximum — running out of wasm heap mid-encode is a worse experience than being told no at the door.
Six ways to make it finish sooner
- Trim instead of converting, when trimming is enough. If the file already plays where you need it to and it is only too long, a stream copy gets you there in milliseconds with zero quality loss. Note that a stream copy lands on a keyframe rather than on the exact second you typed: FFmpeg's own documentation says that "in most formats it is not possible to seek exactly, so ffmpeg will seek to the closest seek point before position," and that when doing stream copy that extra material "will be preserved" instead of being decoded away. So ask for a slightly generous window.
- Trim first, convert second. Re-encoding cost is roughly linear in duration. Cutting a 90-second clip to the 12 seconds you need before converting removes about seven eighths of the work.
- Drop the resolution if the destination is small. Encoding cost tracks pixel count. Something being watched in a chat window does not need the pixels a phone camera recorded.
- Ask for less bitrate. The lower quality presets are not only smaller files, they are less work for the encoder.
- If you only need the sound, extract the audio. Audio encoding is a rounding error next to video encoding — there are no frames to motion-estimate.
- If you only need one image, extract a frame. Decoding to a single timestamp costs a fraction of decoding all of them.
And one that is not a trick: use a desktop machine for the big jobs. A native FFmpeg install with its assembly intact, on a machine with real cores and no 1GB heap, will beat any browser at this. In-browser processing is the right tool when the file is sensitive, when you are on a computer you cannot install software on, or when the alternative is uploading footage to a stranger — not when you are batch-transcoding a hundred gigabytes.
The one fast path, and why it is not always there
There is an exception worth naming, because it is the honest answer to "why is MP4 sometimes instant here and sometimes not offered at all."
NearVid's FFmpeg core is built GPL-free, which means it has no H.264 or H.265 encoder — libx264 and libx265 are GPL-only, and there is no non-GPL substitute inside FFmpeg. Decoding MP4 input is fine; producing MP4 output from that engine is simply not possible. The full story of that build is its own post. So MP4 output comes from somewhere else entirely: the WebCodecs API, which in MDN's words "enables web developers to encode and decode video and audio in the browser efficiently (using hardware acceleration)." Those are the browser's own codecs, not ours, and MDN's codec guide calls H.264 "one of the most widely supported codecs across browsers, operating systems, and consumer devices." When that path is available it is far quicker than the WebAssembly one, because it is not the WebAssembly one.
The catch is that availability is a per-browser, per-device question, not a promise. NearVid probes it at runtime with VideoEncoder.isConfigSupported() for both H.264 video and AAC audio, and separately checks that your specific file can be read, and only then does the MP4 option appear. If it is not there, the browser you are using cannot do it. We would rather hide a card than show a button that fails halfway through, but it does mean two people on two machines can genuinely see different options for the same file — that is a real limitation, not a bug.
The honest summary
Converting video in a browser is slower than converting it on a server because the work moved to
your device and the engine arrived with restrictions: a 25MB WebAssembly core to load, FFmpeg
compiled with --disable-asm and portable 128-bit SIMD in place of per-CPU assembly, and
threading that only exists when the page is cross-origin isolated — worth about 1.93x when it is,
and a hard failure rather than a fallback when it is not. In our measurements an 8-second 640x360
clip stream-copied in 32ms and re-encoded to WebM in 6,940ms on four cores; the two-hundred-fold
ratio between copying and re-encoding is the number to remember, and the millisecond figures are
from one small synthetic clip on one cloud VM, not a promise about your phone. Memory is the hard
edge: a fixed 1024MB wasm heap under a 4GB platform ceiling, which is why files stop at 500MB on
desktop and 50MB on phones and low-memory devices, refused up front rather than mid-encode. Pick
the cheapest operation that solves your problem, trim before you convert, and accept the wait for
what it is — the price of the file never leaving the machine you are sitting at.