Eazip.js
EazipGitHub
Guides

ZIP Large Files in the Browser Without Running Out of Memory

Why big browser ZIPs crash tabs, the mitigations in the order to try them — store-only compression, split output, bounded concurrency — and the honest ceiling where the job belongs elsewhere.

Browser ZIPs fail at scale in one specific way: memory. The tab holds the sources being processed and the growing archive, and when that outgrows what the browser will give a page, the symptoms are a crashed tab, an "Aw, Snap", or a download that silently never arrives. This page is the mitigation list, in the order to try it — and the honest ceiling.

Why it breaks

Three things compete for tab memory: source bytes in flight, compression working space, and the assembled archive waiting to be handed to the download. Their sum tracks the export's total size, so every browser ZIP has a practical ceiling — usually somewhere in the hundreds of megabytes to a few gigabytes, varying by device, browser, and what else the page holds. The failure is architectural, not a bug to fix.

Mitigations, in order

1. Stop compressing what's already compressed. Photos, video, audio, and existing archives don't shrink under Deflate — they just burn CPU and working memory:

const result = await createZip({
  files,
  zipName: 'photos.zip',
  compressionLevel: 0, // store-only
});

For a media-heavy export this is the single biggest win, and the output is barely larger.

2. Split the output into parts. maxZipSizeBytes caps each output ZIP; parts are numbered and each downloads separately:

const result = await createZip({
  files,
  zipName: 'export.zip',
  maxZipSizeBytes: 500_000_000,
});

result.downloadAll();

Smaller parts also fail smaller: a connection hiccup costs one part, not the whole export. Details in Multi-ZIP splitting.

3. Bound fetch concurrency. URL sources are fetched in parallel (default 4 at a time). For very large individual files, lowering concurrency trims the peak of source bytes held simultaneously.

4. Keep partial output on failures. The default already does this — if memory pressure kills individual fetches, the usable archive still ships with the failures reported (Partial results and errors).

The honest ceiling

The mitigations stretch the ceiling; they don't remove it. Multi-gigabyte exports, thousands of files, or downloads that must survive a reload are tab-hostile by nature, and the fix is to move the job out of the tab — same call, two options:

const result = await createZip({
  strategy: 'cloud',
  publicKey: 'pk_ez_...',
  files,
  zipName: 'export.zip',
});

result.downloadAll();

The archive builds on managed Eazip (output scales to 500 GB per job), the tab holds nothing, and delivery is a resumable link. The boundary is mapped in Scale with Eazip.

FAQ

How large is "too large" for a browser ZIP?

There's no fixed number — it depends on the device and what else the page holds. Hundreds of megabytes is routine; past a gigabyte or two you're gambling on your users' hardware, which is the signal to switch paths.

Does splitting reduce memory use or just output size?

Both matter, but its main effect is on output handling and failure isolation. Pair it with compressionLevel: 0 (media) and bounded concurrency for the memory side.

Why does the export work on my machine and fail for users?

Your development machine has more memory and a quieter tab than a customer's four-year-old laptop with sixty tabs. Memory ceilings are the classic works-for-me failure — design for the weak device or move the job server-side.

Is a Web Worker the fix?

A worker moves work off the main thread, which helps responsiveness — but it's the same memory pool. It doesn't raise the ceiling.