main-thread seismograph

ZIP compression that runs on the main thread freezes everything — animation, scrolling, clicks, typing. A Web Worker moves the work off-thread so the page stays alive. This instrument plots per-frame time live: a flat line is a responsive UI; a tall spike is the UI hung. Run the tests and watch which compressors spike.

frame-time monitor

status: idle
live fps 
this frame  ms
worst stall this session  0 ms
baseline 16.7ms = 60fps · spikes scaled √

workload

Loading libraries from CDN…

feel the freeze (interact during a run)

Start a run, then try these. During main-thread tests they go dead; during worker/native they stay buttery.

results

compressor codec time longest main-thread stall janky frames
(>50ms)
output verdict

longest main-thread stall — the punchline

lower is better · bar length = milliseconds the UI was frozen during that test
How to read this honestly — caveats & methodology
  • This measures responsiveness, not codec speed. fflate is a faster DEFLATE engine than JSZipp's in-repo JS deflate, so its codec time column will usually be lower too. The point of the demo is the stall column: how long the main thread was blocked and unable to render or respond.
  • JSZipp appears three times. The main-thread row is the default web-jszipp writer. The worker rows load web-jszipp/worker-plugin, pass a worker backend to the normal ZipWriter, and compare static module-worker and classic-worker scripts.
  • fflate sync (zipSync) also runs on the main thread — included to show that "sync on main thread" is the property that freezes the UI, regardless of library. fflate worker (zip(...), async) ships its own inline Web Worker and transfers the buffer off-thread.
  • Native CompressionStream('deflate-raw') is a bonus row: it runs the codec in native browser code off the main thread with zero added bundle. It emits a raw DEFLATE stream, not a full ZIP container, so its output size isn't directly comparable — it's here to show the cheapest off-thread path.
  • The jank indicators are JS-driven on purpose. The spinner and tap counter update via requestAnimationFrame, so they freeze with the main thread. (A pure-CSS spinner can keep turning on the compositor even while JS is hung, which would hide the problem.)
  • Stall is measured as the largest gap between consecutive animation frames during each test: when JS blocks, rAF stops firing, and the gap ≈ the freeze duration.
  • Warm-up matters. The first run of a library includes JIT warm-up; re-run for steadier numbers. Output sizes differ slightly between deflate implementations and levels — that's expected.

Testing your local JSZipp build instead of the published one? Replace the web-jszipp CDN URL in the source with ../dist/jszipp.umd.js, ../dist/jszipp.worker-plugin.mjs, and ../dist/jszipp.worker.mjs. For compat builds use the matching ../dist/cr61ff58/ or ../dist/cr86ff68/ UMD plugin and classic worker script. Serve the repo over HTTP (e.g. node scripts/serve-demo.mjs), since module/UMD/worker loading needs a real origin.