<?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: CrellinCreative</title>
    <description>The latest articles on DEV Community by CrellinCreative (@crellincreative_).</description>
    <link>https://dev.to/crellincreative_</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%2F4155487%2F95cb1029-bcc8-45f4-a184-df4c6bdf4f25.png</url>
      <title>DEV Community: CrellinCreative</title>
      <link>https://dev.to/crellincreative_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/crellincreative_"/>
    <language>en</language>
    <item>
      <title>Every compression library I tried ran out of RAM. So I built one that doesn't.</title>
      <dc:creator>CrellinCreative</dc:creator>
      <pubDate>Fri, 02 Oct 2026 08:41:05 +0000</pubDate>
      <link>https://dev.to/crellincreative_/every-compression-library-i-tried-ran-out-of-ram-so-i-built-one-that-doesnt-4kma</link>
      <guid>https://dev.to/crellincreative_/every-compression-library-i-tried-ran-out-of-ram-so-i-built-one-that-doesnt-4kma</guid>
      <description>&lt;p&gt;Compress a 1 GB file in the browser with most JavaScript libraries and you'll use &lt;strong&gt;1.6 GB of RAM&lt;/strong&gt;. Try 4 GB and the tab crashes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CrellinCreative/zstd-stream" rel="noopener noreferrer"&gt;zstd-stream&lt;/a&gt; does it in &lt;strong&gt;35 MB&lt;/strong&gt;, whatever the file size, and the file never leaves your device:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compressStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zstd-stream&lt;/span&gt;&lt;span class="dl"&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;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compressStream&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="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any file size, ~35 MB of memory.&lt;/strong&gt; Verified with 4 GB files in Chrome, Edge, Firefox, Safari and Node.js.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beats gzip.&lt;/strong&gt; 5.5× compression at 147 MB/s, vs 3.5× at 51 MB/s for the browser's built-in gzip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One install, nothing else.&lt;/strong&gt; The WebAssembly is embedded: no &lt;code&gt;.wasm&lt;/code&gt; file to host, no CDN, zero dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/zstd-stream" rel="noopener noreferrer"&gt;NPM&lt;/a&gt; · &lt;a href="https://crellincreative.github.io/zstd-stream/" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt; · &lt;a href="https://github.com/CrellinCreative/zstd-stream" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I wanted a simple way to compress and decompress files locally, in my browser, without uploading them anywhere.&lt;/p&gt;

&lt;p&gt;I tried the usual libraries: pako, fflate, then the Zstandard ports zstd-codec and &lt;a class="mentioned-user" href="https://dev.to/bokuweb"&gt;@bokuweb&lt;/a&gt;/zstd-wasm. Small files worked fine. Big files ran the tab out of memory, every time.&lt;/p&gt;

&lt;p&gt;Switching libraries didn't help, because they all share the same design.&lt;/p&gt;
&lt;h2&gt;
  
  
  The 2 GB wall
&lt;/h2&gt;

&lt;p&gt;Almost every JavaScript compression library looks like this:&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;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// whole file in, whole file out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The entire input &lt;em&gt;and&lt;/em&gt; the entire output sit in memory at once. A browser tab can't hold an &lt;code&gt;ArrayBuffer&lt;/code&gt; much past ~2 GB, so anything beyond that: &lt;strong&gt;Finito&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx1kpr6ts8kxlfjjj1ux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx1kpr6ts8kxlfjjj1ux.png" alt="Peak memory compressing 1 GB: pako, fflate and zstd-codec all use 1.6 GB, @bokuweb/zstd-wasm crashes, browser gzip uses 37 MB, zstd-stream uses 35 MB" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No optimisation fixes that. The API shape is the problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: never hold the whole file
&lt;/h2&gt;

&lt;p&gt;zstd-stream takes Meta's open-source Zstandard C library, compiles it to WebAssembly, and puts a streaming layer on top. That streaming layer is the part I wrote.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;ReadableStream&lt;/code&gt; goes in and a &lt;code&gt;ReadableStream&lt;/code&gt; comes out, the same type &lt;code&gt;File.stream()&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt; and Node's &lt;code&gt;Readable.toWeb()&lt;/code&gt; already give you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;128 KB at a time.&lt;/strong&gt; Data flows through fixed buffers, so 4 MB and 4 GB use the same memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpressure.&lt;/strong&gt; If the consumer is slow (a disk, a network), it stops reading the source instead of piling data up in RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checksummed.&lt;/strong&gt; Corrupt or truncated input throws instead of quietly returning wrong bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard &lt;code&gt;.zst&lt;/code&gt; output.&lt;/strong&gt; The &lt;code&gt;zstd&lt;/code&gt; command-line tool opens it, and it opens files made by any other zstd tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it's &lt;strong&gt;completely self-contained&lt;/strong&gt;. The WebAssembly binary is embedded in the package, so there's no &lt;code&gt;.wasm&lt;/code&gt; file to serve, no bundler plugin, no CDN path to get wrong, and zero dependencies. The whole thing is 163 kB installed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Compress a local file, nothing uploaded
&lt;/h2&gt;

&lt;p&gt;This is what I originally wanted. In the browser, pick a file and stream the result straight to disk with &lt;a href="https://github.com/jimmywarting/StreamSaver.js" rel="noopener noreferrer"&gt;StreamSaver&lt;/a&gt;:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compressStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zstd-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;streamSaver&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;streamsaver&lt;/span&gt;&lt;span class="dl"&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;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compressStream&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="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&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;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamSaver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWriteStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.zst`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The same API works in Node.js 18+:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createWriteStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Readable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Writable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compressStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zstd-stream&lt;/span&gt;&lt;span class="dl"&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Readable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toWeb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;big.log&lt;/span&gt;&lt;span class="dl"&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;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compressStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&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;compressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toWeb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createWriteStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;big.log.zst&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;&lt;code&gt;pipeTo&lt;/code&gt; handles backpressure for you: if the disk is slow, compression waits for it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Doesn't the browser already do this?
&lt;/h2&gt;

&lt;p&gt;Partly. Every browser ships &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream" rel="noopener noreferrer"&gt;&lt;code&gt;CompressionStream&lt;/code&gt;&lt;/a&gt; with gzip. If gzip is enough, use it. But on 1 GB of log-style data, zstd wins comfortably:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Compression&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gzip (&lt;code&gt;CompressionStream&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;3.5×&lt;/td&gt;
&lt;td&gt;51 MB/s&lt;/td&gt;
&lt;td&gt;37 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;zstd-stream&lt;/strong&gt; (level 3)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.5×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;147 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  What about native zstd?
&lt;/h2&gt;

&lt;p&gt;Firefox 138+ supports &lt;code&gt;CompressionStream("zstd")&lt;/code&gt;, but nothing else does (yet):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Native zstd&lt;/th&gt;
&lt;th&gt;Firefox 138+&lt;/th&gt;
&lt;th&gt;Chrome / Edge&lt;/th&gt;
&lt;th&gt;Safari&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CompressionStream&lt;/code&gt; / &lt;code&gt;DecompressionStream&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;More importantly when this feature does become widely available, there are no options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No compression level.&lt;/strong&gt; For archiving, that's the one that matters. You compress once and store for years, so level 19 is worth the CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No progress.&lt;/strong&gt; Fine for 2 MB. Not fine for 4 GB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;zstd-stream gives you the same output in every engine, with one code path.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus: compress before you upload
&lt;/h2&gt;

&lt;p&gt;Most apps upload files as-is. That's a lot of wasted waiting:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flz1a6n6oeus8no9d5jqh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flz1a6n6oeus8no9d5jqh.png" alt="Upload time for 1 GB of logs on a 10 Mbps connection: about 13 minutes uncompressed, 3.8 with gzip, 2.4 with zstd-stream" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Generally speaking, &lt;strong&gt;inbound traffic is free&lt;/strong&gt; on AWS, GCP and Azure, so this isn't about your ingress bill. The wins are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users wait less.&lt;/strong&gt; Upload is the slowest part of the trip, and compressing 1 GB takes ~7 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You store less&lt;/strong&gt;, possibly forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downloads cost less.&lt;/strong&gt; Egress &lt;em&gt;is&lt;/em&gt; billed, and the file leaves your bucket compressed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No server-side compression job&lt;/strong&gt; re-reading every file.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Uploading large files
&lt;/h2&gt;

&lt;p&gt;In Chrome and Edge, the compressed stream can go straight into &lt;code&gt;fetch&lt;/code&gt; (your server needs HTTP/2 or later):&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/upload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Encoding&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;zstd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duplex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;half&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Firefox and Safari don't support streaming request bodies yet. There, upload the compressed stream in parts instead. Here's a minimal example (status checks and retries left out):&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;compressed&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uploads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;uploads&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/upload/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&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;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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;part&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="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;send&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;uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 3 in flight? wait for the oldest&lt;/span&gt;
  &lt;span class="p"&gt;}&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;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It collects compressed output into 8 MB parts and keeps a few uploading while it compresses the next one. When enough parts are in flight, it simply stops reading, and compression pauses until the network catches up:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudkcdwl8tzaam0enjcq5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fudkcdwl8tzaam0enjcq5.gif" alt="Animation: a file streams through compressStream into 8 MB parts. Several parts upload in parallel while the next one is compressed. Compression only pauses when the upload slots are full, and memory stays flat" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Part size and the number of parallel uploads are up to you; memory stays bounded either way. Parts also map neatly onto S3 multipart uploads, so a failed part can be retried on its own.&lt;/p&gt;
&lt;h2&gt;
  
  
  Downloads work too
&lt;/h2&gt;

&lt;p&gt;Serve the &lt;code&gt;.zst&lt;/code&gt; as-is and decompress it in the browser as it downloads. The bytes you pay egress on stay compressed, and the user's RAM stays flat:&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/exports/logs.csv.zst&lt;/span&gt;&lt;span class="dl"&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;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;decompressStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;restored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamSaver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWriteStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logs.csv&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;Want to process it instead of saving it? Add &lt;code&gt;.pipeThrough(new TextDecoderStream())&lt;/code&gt; and handle the text chunk by chunk as it arrives.&lt;/p&gt;


&lt;h2&gt;
  
  
  Before you ship it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;⚠️ Use a Web Worker.&lt;/strong&gt; Compression is CPU work; keep it off the main thread, otherwise your interface will become sluggish / freeze.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip already-compressed files.&lt;/strong&gt; JPEG, MP4 and ZIP won't shrink much more.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://crellincreative.github.io/zstd-stream/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; is the tool I originally wanted: compress or decompress any file, entirely in your browser. Throw something big at it.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;zstd-stream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/CrellinCreative" rel="noopener noreferrer"&gt;
        CrellinCreative
      &lt;/a&gt; / &lt;a href="https://github.com/CrellinCreative/zstd-stream" rel="noopener noreferrer"&gt;
        zstd-stream
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      High-performance Zstandard compression for Node.js and browsers with zero external dependencies
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;zstd-stream&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Efficient Zstandard compression for Browsers and Node.js&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/zstd-stream" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e291b8526cf481c46c48d582e8970db4f91508b5f41eccd8fdfdb6a16841d8e4/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f7a7374642d73747265616d2e737667" alt="npm version"&gt;&lt;/a&gt;
&lt;a href="https://www.npmjs.com/package/zstd-stream" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c90f5889ea9ad6f9ba457277bee00986655d901d87930585a04ca9fe88fab804/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f696e7374616c6c25323073697a652d3136332532306b422d626c75652e737667" alt="install size"&gt;&lt;/a&gt;
&lt;a href="https://opensource.org/licenses/MIT" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://www.typescriptlang.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fc14c4adf86711006eb319175f774056958caff49249864261895602fadb0bfc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f547970655363726970742d52656164792d627269676874677265656e2e737667" alt="TypeScript"&gt;&lt;/a&gt;
&lt;a href="https://crellincreative.github.io/zstd-stream/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5274b4e2c45a4fe1b80fbce179f9d2a9c6f930ffd1abd2fa823b9530b42194fd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64656d6f2d6c6976652d627269676874677265656e" alt="Browser Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;&lt;code&gt;zstd-stream&lt;/code&gt; compresses and decompresses &lt;a href="https://github.com/facebook/zstd" rel="noopener noreferrer"&gt;Zstandard&lt;/a&gt;
data through the Web Streams API. It streams files larger than available RAM in
&lt;strong&gt;constant memory&lt;/strong&gt;, propagates &lt;strong&gt;backpressure&lt;/strong&gt; end to end, and ships the
WebAssembly build &lt;strong&gt;embedded&lt;/strong&gt; — no &lt;code&gt;.wasm&lt;/code&gt; asset to host or configure.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🌊 &lt;strong&gt;Streaming first&lt;/strong&gt; — process multi-GB data in fixed ~128 KB buffers&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Backpressure built in&lt;/strong&gt; — a slow writer automatically throttles the reader, so memory never runs away&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Zero external assets&lt;/strong&gt; — WASM is bundled in; &lt;code&gt;npm install&lt;/code&gt; and import&lt;/li&gt;
&lt;li&gt;🛡️ &lt;strong&gt;Integrity checked&lt;/strong&gt; — every frame carries a checksum; corrupt or truncated input is rejected&lt;/li&gt;
&lt;li&gt;🌍 &lt;strong&gt;Universal&lt;/strong&gt; — the same code runs in Node.js 18+ and every modern browser&lt;/li&gt;
&lt;li&gt;🔧 &lt;strong&gt;ESM + TypeScript&lt;/strong&gt; — full type definitions included&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Progress callbacks&lt;/strong&gt; — observe bytes processed in real time&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Unrivaled performance&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Compressing &lt;strong&gt;1 GB&lt;/strong&gt; — &lt;code&gt;zstd-stream&lt;/code&gt; vs. the browser-capable libraries…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/CrellinCreative/zstd-stream" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
      <category>webassembly</category>
    </item>
  </channel>
</rss>
