Skip to content

[Feature] Migrate ZIP library from JSZip to fflate #1475

@cyfung1031

Description

@cyfung1031

Migrate ZIP library from JSZip to fflate

fflate-vs-jszip-benchmark.html

fflate-vs-jszip-benchmark-v2.html

Summary

Replace jszip with fflate for all ZIP generation. fflate is significantly faster, smaller, and actively maintained.

Motivation

We benchmarked both libraries in-browser under identical conditions (same input data, DEFLATE level 6, fixed UTC timestamp, 16 files across 8 directories):

  • Speed — fflate's synchronous zipSync consistently outperforms JSZip's generateAsync by a significant margin across all payload types (text, binary, repetitive, mixed) and file sizes (10 KB – 1 MB per file)
  • Bundle size — fflate is ~8 KB minified+gzipped vs JSZip's ~100 KB
  • Output correctness — both libraries produce valid, extractable ZIP files with identical compressed data blocks at the same compression level. The only structural difference is that fflate writes an extended-timestamp extra field (0x5455, 9 bytes per entry) which is a strictly additive and standards-compliant improvement

Migration notes

API difference to be aware of — timestamps:
JSZip's date option is always interpreted as UTC. fflate's mtime behaves the same way. Pass new Date(Date.UTC(...)) explicitly rather than new Date(...) (local time) to ensure the encoded DOS timestamp is identical regardless of the user's timezone.

// ❌ Before (JSZip)
const zip = new JSZip();
zip.file('foo.txt', data, {
  compression: 'DEFLATE',
  compressionOptions: { level: 6 },
  date: new Date(2000, 0, 1),        // ⚠️ local time — shifts by timezone in JSZip
});
const out = await zip.generateAsync({ type: 'uint8array' });

// ✅ After (fflate)
const out = fflate.zipSync({
  'foo.txt': [data, {
    level: 6,                          // must be set per-entry; top-level option is ignored
    mtime: new Date(Date.UTC(2000, 0, 1)), // explicit UTC
  }],
});

Other gotchas:

  • level must be specified per entry in fflate — passing it to zipSync at the top level is silently ignored
  • crypto.getRandomValues() is capped at 65 536 bytes per call; if generating random binary test data, chunk calls accordingly
  • fflate's zipSync is synchronous and blocks the main thread for large inputs — use zip (async/worker-based) for files above a few MB if responsiveness matters

Acceptance criteria

  • jszip removed from dependencies
  • fflate added to dependencies
  • All ZIP generation paths migrated to fflate.zipSync (or fflate.zip for large async cases)
  • Timestamps passed as Date.UTC(...) throughout
  • Existing tests pass; ZIP outputs are valid and extractable

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions