Mastering Client-Side File Streaming with StreamSaver.js

StreamSaver.js enables web applications to save files directly to the client's file system using streaming technology. This approach mitigates memory constraints associated with large blobs and prevents browsers from attempting to preview content instead of downloading it.

Traditional methods involving Blob objects require loading the entire file into memory before triggering a download. For large datasets, this causes performance degradation. Additionally, certain MIME types like text/plain or video/mp4 may open in a new tab rather than saving. StreamSaver.js bypasses these limitations by establishing a download stream that writes data chunks directly to the disk.

Installation

The library is available via package managers or direct script inclusion.

NPM

npm install streamsaver

Import within your module:

import streamSaver from 'streamsaver';

CDN Include the script tag in your HTML head or body:

<script src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.5/dist/StreamSaver.js"></script>

Basic File Download

To prevent the browser from rendering text files inline, initialize a writable stream. The process involves creating the stream, fetching the remote resource, and piping the response body to the stream.

<button id="btn-save">Save Report</button>

<script src="StreamSaver.js"></script>
<script>
  document.getElementById('btn-save').addEventListener('click', async () => {
    // Initialize the writable stream with the desired filename
    const outputStream = streamSaver.createWriteStream('report.txt');

    try {
      const response = await fetch('/assets/report.txt');
      const readable = response.body;

      // Check for native pipeTo support
      if (window.WritableStream && readable.pipeTo) {
        await readable.pipeTo(outputStream);
        console.log('Transfer complete');
        return;
      }

      // Fallback for browsers without pipeTo
      const writer = outputStream.getWriter();
      const reader = readable.getReader();

      const pump = async () => {
        const { done, value } = await reader.read();
        if (done) {
          await writer.close();
          return;
        }
        await writer.write(value);
        await pump();
      };

      await pump();
    } catch (err) {
      console.error('Download failed', err);
    }
  });
</script>

Note: If cross-origin issues arise, configure the mitm property to point to a hosted mitm.html file provided in the library repository.

streamSaver.mitm = 'https://your-domain.com/mitm.html';

Archiving Multiple Files

Combining StreamSaver.js with a ZIP utility allows for bundling multiple resources into a single archive before saving. This requires the zip-stream.js utility often found in the examples directory of the library.

<button id="btn-archive">Download Archive</button>
<script src="StreamSaver.js"></script>
<script src="zip-stream.js"></script>
<script>
  const resources = [
    { name: 'data.csv', src: '/assets/data.csv' },
    { name: 'log.txt', src: '/assets/log.txt' }
  ];

  document.getElementById('btn-archive').addEventListener('click', () => {
    const fileStream = streamSaver.createWriteStream('backup.zip');

    const zipStream = new ZIP({
      async pull(controller) {
        for (const item of resources) {
          const resp = await fetch(item.src);
          controller.enqueue({
            name: item.name,
            stream: () => resp.body
          });
        }
        controller.close();
      }
    });

    if (window.WritableStream && zipStream.pipeTo) {
      zipStream.pipeTo(fileStream).then(() => console.log('Archive saved'));
    }
  });
</script>

Concatenating Streams

Data from multiple sources can be merged into a single file during the download process. For instance, combiinng several CSV fragments into one continuous file requires managing the write stream manually to insert delimiters between chunks.

<button id="btn-merge" onclick="initiateMerge()">Merge & Download</button>
<script src="StreamSaver.js"></script>
<script>
  const encoder = new TextEncoder();
  const sources = [
    '/assets/part1.csv',
    '/assets/part2.csv'
  ];
  let streamWriter = null;
  let sourceIterator = null;

  async function initiateMerge() {
    const outStream = streamSaver.createWriteStream('combined.csv');
    streamWriter = outStream.getWriter();
    sourceIterator = sources[Symbol.iterator]();
    await processNextSource();
  }

  async function processNextSource() {
    const { done, value } = sourceIterator.next();

    if (done) {
      await streamWriter.close();
      return;
    }

    const response = await fetch(value);
    const reader = response.body.getReader();

    while (true) {
      const { done: readDone, value: chunk } = await reader.read();
      if (readDone) break;
      await streamWriter.write(chunk);
    }

    // Append newline between files
    await streamWriter.write(encoder.encode('\n'));
    await processNextSource();
  }
</script>

Tags: javascript streaming file-download web-development streamsaver

Posted on Tue, 02 Jun 2026 17:32:23 +0000 by harkonenn