# StreamSaver.js: Writing Large Browser-Generated Files Without Buffering Them in RAM

> StreamSaver.js emulates a server download from inside the page, using a service worker and a hidden iframe so a web app can write bytes straight to disk. It is the right tool for client-generated data and the wrong one for files that already sit on a server.

**jimmywarting/StreamSaver.js** — StreamSaver writes stream to the filesystem directly asynchronous

- Repository: https://github.com/jimmywarting/StreamSaver.js
- Website: https://jimmywarting.github.io/StreamSaver.js/example.html
- Stars: 4,370 · Forks: 442
- Language: JavaScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/jimmywarting-streamsaver-js

## The blob ceiling that StreamSaver.js exists to avoid

FileSaver.js made saving blobs easy, and the README thanks Eli Grey for it. The obstacle it names is memory: a blob has to exist in RAM before it can be handed to the browser, and large exports push against both available memory and the maximum blob size. StreamSaver.js takes a different route. The README states that instead of saving data in client-side storage or in memory, you create a writable stream directly to the file system, and that this is not Chrome's sandboxed file system or any other web storage. The mechanism is an emulation of how a server instructs a browser to save a file, built from response headers plus a service worker.

The audience follows from that. This is for web apps that generate large amounts of data on the client, on devices with limited RAM. The README is explicit about the inverse case: if the file comes from the cloud or a server, use the server, add the response headers, and do not use AJAX to fetch it. StreamSaver is described as a last resort when you cannot change those headers. It is for client-generated content inside the browser.

## How the service worker and mitm.html turn a stream into a download

The repository layout shows the moving parts: StreamSaver.js is the library, mitm.html is the man-in-the-middle page, and sw.js is the service worker. The library creates a WritableStream that only accepts Uint8Array chunks. No other typed arrays, no ArrayBuffers and no strings are allowed, so anything else has to be converted first. The README suggests Response as the conversion tool, since a Response body is a byte stream that can be piped into the writable stream.

The service worker is what makes the bytes land on disk. When the stream is created, the worker responds to a navigation with the stream as the response body, which is why the browser treats it as an ordinary download. The README notes that when transferable streams are supported through postMessage, the worker does not have to handle any logic at all, because the stream transferred to the worker becomes the response. Where they are not supported, the worker manages the transfer itself, and it can go idle after 30 seconds in Firefox and 5 minutes in Blink. The README says an https mitm iframe can ping the worker to prevent that.

The size option is optional and only affects progress reporting. Passing size does not reserve space or change the write path.

## Installing StreamSaver.js from npm and saving a first file

The package is published as streamsaver, with StreamSaver.js as the main entry, and the README's simplest example loads a ponyfill and the library from jsDelivr. The same example shows the three ways the module is exposed: an import, a require, and the window global.

```html
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js"></script>
<script>
  import streamSaver from 'streamsaver'
  const streamSaver = require('streamsaver')
  const streamSaver = window.streamSaver
</script>
```

After the script tags, window.streamSaver is the object you call. The README's first real use encodes a string and creates a write stream with a name and an optional size, then either writes manually through a writer or pipes a Response body into the stream. The size field is what lets the browser show progress.

```js
const uInt8 = new TextEncoder().encode('StreamSaver is awesome')

const fileStream = streamSaver.createWriteStream('filename.txt', {
  size: uInt8.byteLength,
  writableStrategy: undefined,
  readableStrategy: undefined
})

new Response('StreamSaver is awesome').body
  .pipeTo(fileStream)
  .then(success, error)
```

Two configuration points matter early. StreamSaver can detect and use a ponyfill loaded from a CDN, and if you host the mitm page and service worker yourself, you point the library at your copy with streamSaver.mitm. The README's example value is a custom mitm.html URL on your own domain.

```js
streamSaver.WritableStream = streamSaver.WritableStream
streamSaver.TransformStream = streamSaver.TransformStream
streamSaver.mitm = 'https://example.com/custom_mitm.html'
```

One practical constraint from the README: some browsers have ReadableStream but not WritableStream, and the web-streams-polyfill project covers that gap. The README prefers the ponyfill over the polyfill, because StreamSaver works better when a native ReadableStream is transferable to the service worker. If you cannot serve over https, initiate createWriteStream on user interaction so the popup that installs the worker is not blocked.

## Leaving the page breaks the download, and the library cannot stop you

This is the failure mode to design around. Because the download looks like a normal native download, users assume the browser is fetching it in the background and leave the page. It is not. The README states plainly that the download gets broken when you leave the page, and gives unload handlers that abort the writable stream and the writer so the download does not look stuck.

```js
window.onunload = () => {
  writableStream.abort()
  writer.abort()
}

window.onbeforeunload = evt => {
  if (!done) {
    evt.returnValue = `Are you sure you want to leave?`;
  }
}
```

There is a wrinkle in insecure contexts. The README says that without a secure context, StreamSaver navigates to the download URL instead of using a hidden iframe, which triggers onbeforeunload when the download starts but does not call onunload. In a secure context the handler can be added immediately; otherwise it has to be added later. That timing difference is a real source of bugs and the README does not offer a single pattern that covers both cases.

The second limitation is scope. If the file already lives on a server, StreamSaver is the wrong tool. The README says to add the extra Response headers on the server and not use AJAX, and calls StreamSaver a last resort when you cannot change those headers. Emulating a download to move bytes that a server could have sent directly adds a service worker, a mitm page and an unload handler for no benefit.

## StreamSaver.js against FileSaver.js and the native File System API

FileSaver.js saves files and blobs easily, and the README credits it before explaining why it is not enough: the RAM it can hold and the max blob size limitation. The difference in approach is where the bytes live. FileSaver builds the whole blob first, so peak memory scales with file size. StreamSaver never holds the file; chunks go into a writable stream that the service worker turns into a download, so memory stays flat regardless of total size. If your payload is a few kilobytes, FileSaver is simpler and has no worker or mitm page to configure.

The other alternative is the platform itself. The README points to the whatwg/fs work on saving files to the HD and says it will more or less make FileSaver, StreamSaver and similar packages a bit obsolete in the future. It also notes that the API is still experimental and not implemented by all browsers, which is why the author built native-file-system-adapter to expose it in all browsers, Deno and NodeJS with different storages. The honest comparison is that StreamSaver is a workaround for an API that is arriving, and its value today is browser coverage rather than the mechanism itself.

## Maintenance, release cadence and what the MIT licence leaves you to decide

The last push to the repository was on 2026-07-30, so the project is not dormant, but the release history tells a different story. The most recent release is 2.0.6 from 2022-02-11, after 2.0.4 in 2020 and 2.0.3 in 2019. The README itself opens with "legacy-ish" and says it is not deprecated, still maintained, and still recommended when needed. Treat the npm version and the repository head as two different things: pinning streamsaver@2.0.6 means you are on a release that predates roughly four years of commits, and the README's own examples reference 2.0.3 from the CDN.

Upgrade cost is low in one sense and awkward in another. There is no build step in package.json, the test script exits successfully without running anything, and the library is a single file plus mitm.html and sw.js. That also means there is no test suite to lean on when you upgrade, and no automated signal that a change is safe. You verify behaviour in a browser.

The licence is MIT, which permits commercial and closed-source use and requires that the copyright notice and permission notice travel with copies or substantial portions. That is a summary of the identifier, not legal advice; if you host mitm.html and sw.js yourself, check what your own distribution and caching setup does with the notice.

## Conclusion

Adopt StreamSaver.js when the bytes are produced in the browser and the blob would be too large for memory: exports, recordings, archives, anything assembled client-side. Skip it when the file already exists on a server, because the README says to add the response headers there and avoid AJAX instead. Before shipping, verify three things on your own target browsers: that the service worker registers from your mitm.html, that leaving the page aborts the stream the way your unload handler expects, and whether transferable streams are available, since the worker idle timers differ between Firefox and Blink.

## FAQ

### How do I install StreamSaver.js from npm?

The package is published as streamsaver, with StreamSaver.js as its main entry, so you install it from npm and import it as a module. The README's simplest example instead loads the library and a web-streams ponyfill from jsDelivr and uses the window.streamSaver global.

### Does StreamSaver.js work in React?

Nothing in the README ties the library to a framework; it exposes a module import, a require and a window global. The parts that need care in a component tree are the unload handlers, which the README attaches to window, and starting createWriteStream on user interaction when you are not in a secure context.

### Why does my StreamSaver.js download stop when I leave the page?

The README states that the download gets broken when you leave the page, even though it looks like a regular background download. It gives unload handlers that call abort on the writable stream and the writer, plus an onbeforeunload confirmation, so the transfer ends cleanly instead of appearing stuck.

### Should I use StreamSaver.js or FileSaver.js?

FileSaver.js saves files and blobs but is limited by the RAM it can hold and the maximum blob size, according to the README. StreamSaver.js writes through a service worker instead of holding the file in memory, which is the reason to pick it for large client-generated data.

## Sources

- [jimmywarting/StreamSaver.js on GitHub](https://github.com/jimmywarting/StreamSaver.js)
- [License: MIT](https://github.com/jimmywarting/StreamSaver.js/blob/master/LICENSE)
- [Project website](https://jimmywarting.github.io/StreamSaver.js/example.html)
- [README](https://github.com/jimmywarting/StreamSaver.js/blob/master/README.md)
- [Releases](https://github.com/jimmywarting/StreamSaver.js/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jimmywarting-streamsaver-js
