<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fei Y</title>
    <description>The latest articles on DEV Community by Fei Y (@feiy_redpanda).</description>
    <link>https://dev.to/feiy_redpanda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4129411%2F82ca143d-7ffb-4cad-951d-370251b70c5d.jpeg</url>
      <title>DEV Community: Fei Y</title>
      <link>https://dev.to/feiy_redpanda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/feiy_redpanda"/>
    <language>en</language>
    <item>
      <title>Handling Files Over 2GB with WebAssembly in the Browser</title>
      <dc:creator>Fei Y</dc:creator>
      <pubDate>Fri, 18 Sep 2026 01:51:05 +0000</pubDate>
      <link>https://dev.to/feiy_redpanda/handling-files-over-2gb-with-webassembly-in-the-browser-1cmm</link>
      <guid>https://dev.to/feiy_redpanda/handling-files-over-2gb-with-webassembly-in-the-browser-1cmm</guid>
      <description>&lt;p&gt;At &lt;a href="https://www.redpandacompress.com" rel="noopener noreferrer"&gt;RedPandaCompress&lt;/a&gt; people routinely drop 4-8GB video files into the browser and expect them to compress or convert without ever touching a server. No upload, no queue, no "your file is processing, check back later" email. Everything happens client-side, in WebAssembly.&lt;/p&gt;

&lt;p&gt;The catch: WebAssembly was never really designed for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall you hit first
&lt;/h2&gt;

&lt;p&gt;A wasm module's linear memory is one contiguous, growable &lt;code&gt;ArrayBuffer&lt;/code&gt;. In practice you hit trouble long before the theoretical 4GB (32-bit) ceiling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every growth step needs a &lt;strong&gt;new&lt;/strong&gt; buffer big enough to hold the old contents &lt;em&gt;plus&lt;/em&gt; the increase, allocated contiguously, before the old one is freed. That doubles your peak requirement right at the moment you're already low on headroom.&lt;/li&gt;
&lt;li&gt;Browsers cap single &lt;code&gt;ArrayBuffer&lt;/code&gt; allocations well under 4GB in practice, and you'll see it as a plain &lt;code&gt;RangeError: Array buffer allocation failed&lt;/code&gt; — not an out-of-memory crash, just a hard refusal.&lt;/li&gt;
&lt;li&gt;None of this is optional if your naive approach is "read the whole file, hand the bytes to the wasm module." A 3GB input file, loaded into a wasm-visible buffer, is already most of the way to that wall — before you've decoded a single frame.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the fix isn't "increase the memory limit." It's making sure the whole file never has to live in one contiguous wasm-addressable buffer in the first place — at any stage: input, processing, or output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input: don't copy the file in, mount it
&lt;/h2&gt;

&lt;p&gt;The obvious approach — read the &lt;code&gt;File&lt;/code&gt; object into an &lt;code&gt;ArrayBuffer&lt;/code&gt; with &lt;code&gt;file.arrayBuffer()&lt;/code&gt; and write it into the wasm filesystem (MEMFS) — means the entire input now exists twice: once as a JS &lt;code&gt;ArrayBuffer&lt;/code&gt;, once copied into the wasm heap. For an 8GB file that's an 8GB tax before any real work starts.&lt;/p&gt;

&lt;p&gt;ffmpeg.wasm (and Emscripten's FS layer generally) supports mounting a filesystem backed directly by a &lt;code&gt;Blob&lt;/code&gt;/&lt;code&gt;File&lt;/code&gt;, instead of copying bytes into MEMFS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FFFSType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WORKERFS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/data/input.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-c:v&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unmount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WORKERFS reads lazily, straight from the browser's own &lt;code&gt;File&lt;/code&gt;/&lt;code&gt;Blob&lt;/code&gt; (which is typically backed by disk, not RAM) whenever ffmpeg actually asks for a byte range. The file is never copied into the wasm heap wholesale. This one change is what makes "8GB input" a non-event instead of an immediate wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output: don't collect it in memory either
&lt;/h2&gt;

&lt;p&gt;The other direction has the same trap. If ffmpeg's output lands in wasm's MEMFS and you then read it out as a single &lt;code&gt;Uint8Array&lt;/code&gt; to build a &lt;code&gt;Blob&lt;/code&gt;, you've reintroduced the exact problem you just solved on the input side — the whole output now has to exist as one contiguous buffer before you can hand it back to the page.&lt;/p&gt;

&lt;p&gt;The fix is symmetric: stream it out in chunks instead of pulling one giant buffer at the end. Read the output progressively and assemble it as &lt;code&gt;Blob&lt;/code&gt; parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video/mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome (and most modern browsers) can spill large &lt;code&gt;Blob&lt;/code&gt;s to disk rather than holding them entirely in RAM, so building the result as many small parts — instead of one &lt;code&gt;ArrayBuffer&lt;/code&gt; the size of the whole output — sidesteps the same allocation ceiling on the way out.&lt;/p&gt;

&lt;h2&gt;
  
  
  For genuinely huge jobs: bound the work, not just the memory
&lt;/h2&gt;

&lt;p&gt;Even with lazy input and streamed output, a single ffmpeg invocation over the &lt;em&gt;entire&lt;/em&gt; file still has to hold its own working state proportional to what it's processing. For very large compress jobs we split the source into bounded segments, run each one through its own short-lived ffmpeg pass, and concatenate the results — rather than asking one wasm instance to eat the whole file in one go. It's the same principle one level up: cap how much any single unit of work has to hold in memory at once, regardless of how large the total job is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;There's no single trick that makes "&amp;gt;2GB in WebAssembly" work. It's the same discipline applied at every boundary: never let the &lt;em&gt;whole&lt;/em&gt; file — input, intermediate state, or output — become one contiguous thing that has to live entirely inside wasm-addressable memory at once. Mount instead of copy. Stream instead of collect. Segment instead of one giant pass.&lt;/p&gt;

&lt;p&gt;This is the first of what I expect will be a few posts on the weirder corners of shipping a real, no-upload media tool as a client-side wasm app — happy to go deeper on any of this if people are curious.&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>javascript</category>
      <category>ffmpeg</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
