PatternsAssets Engine
Download Stream Saver
Download large files using Streams API without loading everything into RAM.
The Problem
Downloading a 2GB file with response.blob() loads the entire file into memory, causing crashes on devices with limited RAM.
Solution
async function downloadWithStreams(url: string, filename: string): Promise<void> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`);
}
const contentLength = response.headers.get('Content-Length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
let loaded = 0;
const chunks: Uint8Array[] = [];
const reader = response.body?.getReader();
if (!reader) {
throw new Error('ReadableStream not supported');
}
// Read stream in chunks
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.byteLength;
// Show progress
if (total) {
const percent = (loaded / total) * 100;
console.log(`Downloaded: ${percent.toFixed(1)}%`);
}
}
// Create blob and download
const blob = new Blob(chunks);
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(downloadUrl), 100);
}
// With File System Access API (save directly to disk)
interface FileSystemFileHandle {
createWritable(): Promise<FileSystemWritableFileStream>;
}
interface FileSystemWritableFileStream extends WritableStream {
write(data: BufferSource | Blob | string): Promise<void>;
close(): Promise<void>;
}
async function downloadDirectToDisk(url: string, suggestedName: string): Promise<void> {
// Check browser support
if (!('showSaveFilePicker' in window)) {
throw new Error('File System Access API not supported');
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`);
}
// Show save dialog
const windowWithFS = window as Window & {
showSaveFilePicker?: (options: {
suggestedName: string;
types: Array<{ description: string; accept: Record<string, string[]> }>;
}) => Promise<FileSystemFileHandle>;
};
const handle = await windowWithFS.showSaveFilePicker!({
suggestedName,
types: [{
description: 'All Files',
accept: { '*/*': [] }
}]
});
// Create writable stream
const writable = await handle.createWritable();
// Pipe response directly to file (never enters RAM)
await response.body?.pipeTo(writable);
console.log('File saved successfully');
}
// Chunked download with retry
class ChunkedDownloader {
async download(
url: string,
filename: string,
chunkSize = 5 * 1024 * 1024 // 5MB chunks
): Promise<void> {
// Get file size
const headResponse = await fetch(url, { method: 'HEAD' });
const contentLength = headResponse.headers.get('Content-Length');
if (!contentLength) {
throw new Error('Server does not support range requests');
}
const fileSize = parseInt(contentLength, 10);
const chunks: Uint8Array[] = [];
// Download in chunks
for (let start = 0; start < fileSize; start += chunkSize) {
const end = Math.min(start + chunkSize - 1, fileSize - 1);
const chunk = await this.downloadChunk(url, start, end);
chunks.push(chunk);
const progress = ((end + 1) / fileSize) * 100;
console.log(`Progress: ${progress.toFixed(1)}%`);
}
// Combine and save
const blob = new Blob(chunks);
this.triggerDownload(blob, filename);
}
private async downloadChunk(
url: string,
start: number,
end: number,
retries = 3
): Promise<Uint8Array> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, {
headers: { Range: `bytes=${start}-${end}` }
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error) {
if (attempt === retries) {
throw new Error(`Chunk download failed after ${retries} attempts`);
}
// Exponential backoff
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
throw new Error('Download failed');
}
private triggerDownload(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 100);
}
}
// Usage
await downloadWithStreams(
'https://example.com/large-file.zip',
'download.zip'
);
// Or direct to disk (Chrome 86+)
await downloadDirectToDisk(
'https://example.com/video.mp4',
'my-video.mp4'
);
// Or chunked with retry
const downloader = new ChunkedDownloader();
await downloader.download(
'https://example.com/huge-file.bin',
'file.bin',
10 * 1024 * 1024 // 10MB chunks
);Performance Note
Benefit: Memory usage stays constant (~10-20MB) regardless of file size. A 2GB download uses the same RAM as a 10MB download. File System Access API writes directly to disk, using near-zero browser memory.