@zip.js/zip.js 2.11.1 → 2.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BENCHMARKS.md +268 -93
- package/deno.json +1 -1
- package/dist/zip-core-external.js +56 -19
- package/dist/zip-core-external.min.js +1 -1
- package/dist/zip-core.js +58 -19
- package/dist/zip-core.min.js +1 -1
- package/dist/zip-fs-core-external.js +63 -22
- package/dist/zip-fs-core-external.min.js +1 -1
- package/dist/zip-fs-core.js +65 -22
- package/dist/zip-fs-core.min.js +1 -1
- package/dist/zip-fs-external.js +63 -22
- package/dist/zip-fs-external.min.js +1 -1
- package/dist/zip-fs-native.js +65 -22
- package/dist/zip-fs-native.min.js +1 -1
- package/dist/zip-fs.js +65 -22
- package/dist/zip-fs.min.js +1 -1
- package/dist/zip-legacy.js +58 -19
- package/dist/zip-legacy.min.js +1 -1
- package/dist/zip-native.js +58 -19
- package/dist/zip-native.min.js +1 -1
- package/dist/zip.js +58 -19
- package/dist/zip.min.js +1 -1
- package/index-native.cjs +64 -21
- package/index-native.min.js +1 -1
- package/index.cjs +64 -21
- package/index.d.cts +30 -6
- package/index.d.ts +30 -6
- package/index.min.js +1 -1
- package/lib/core/io.js +2 -3
- package/lib/core/version.js +1 -1
- package/lib/core/zip-fs.js +4 -2
- package/lib/core/zip-writer.js +61 -15
- package/lib/zip-core-writer.js +2 -0
- package/package.json +1 -1
package/BENCHMARKS.md
CHANGED
|
@@ -15,8 +15,10 @@ should not trust anyone's benchmark (including this one) without reproducing it.
|
|
|
15
15
|
> **without spawning a single Web Worker** — it lets the platform's native
|
|
16
16
|
> `CompressionStream` run on the threadpool while you simply issue concurrent `add()`
|
|
17
17
|
> calls. It also streams arbitrarily large files at a flat, low memory ceiling. For
|
|
18
|
-
>
|
|
19
|
-
>
|
|
18
|
+
> archives made of thousands of tiny entries, **fflate** is several times faster and much
|
|
19
|
+
> lighter — but that gap is zip.js's per-entry orchestration, not its codec: measured on
|
|
20
|
+
> their own, the codecs are the other way round (see [Codecs](#codecs-compared-at-equal-output-size)).
|
|
21
|
+
> Against native tooling, zip.js on a modern runtime is a bit
|
|
20
22
|
> faster *and* tighter than 7-Zip's fast mode on single-file streaming; 7-Zip keeps a
|
|
21
23
|
> real edge only on thousands of tiny files.
|
|
22
24
|
|
|
@@ -25,12 +27,13 @@ should not trust anyone's benchmark (including this one) without reproducing it.
|
|
|
25
27
|
| | |
|
|
26
28
|
|---|---|
|
|
27
29
|
| Machine | Apple M2, 8 cores (4 performance + 4 efficiency), 16 GB RAM |
|
|
28
|
-
| OS | macOS 26.
|
|
29
|
-
| Runtime | Node.js
|
|
30
|
-
| zip.js | 2.
|
|
30
|
+
| OS | macOS 26.6.2 (arm64) |
|
|
31
|
+
| Runtime | Node.js v26.7.0 |
|
|
32
|
+
| zip.js | 2.11.2 |
|
|
31
33
|
| jszip | 3.10.1 |
|
|
32
34
|
| fflate | 0.8.3 |
|
|
33
35
|
| archiver | 8.0.0 |
|
|
36
|
+
| Measured | 2026-09-06 — the 7-Zip section under Deno 2.9.6, everything else on Node |
|
|
34
37
|
|
|
35
38
|
## Method
|
|
36
39
|
|
|
@@ -40,11 +43,15 @@ should not trust anyone's benchmark (including this one) without reproducing it.
|
|
|
40
43
|
- **Timing.** `performance.now()` around the measured operation only. Every combination
|
|
41
44
|
runs **3 times**; the table reports the **median**.
|
|
42
45
|
- **Memory.** Peak resident set size (RSS) reported by `/usr/bin/time -l`. The "peak"
|
|
43
|
-
column is the delta over an empty-process baseline (~
|
|
44
|
-
attributable to the work.
|
|
45
|
-
- **Fair work.** All libraries compress at **DEFLATE level 6
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
column is the delta over an empty-process baseline (~50 MB on this runtime), i.e. the
|
|
47
|
+
memory attributable to the work.
|
|
48
|
+
- **Fair work.** All libraries compress at **DEFLATE level 6**, and output sizes are shown
|
|
49
|
+
next to the times. Read them: a "level" is not a unit shared between libraries. zlib's
|
|
50
|
+
level 6 and fflate's level 6 are different parameter sets, so the two do not produce the
|
|
51
|
+
same amount of compression and a time is only comparable next to the size it achieved.
|
|
52
|
+
The [Codecs](#codecs-compared-at-equal-output-size) section compares them by output size
|
|
53
|
+
instead, which is the only axis on which "faster" means anything. The corpus is generated
|
|
54
|
+
from a seeded PRNG, so every library sees byte-for-byte identical input.
|
|
48
55
|
- **zip.js modes.** zip.js is measured both **single-threaded** (apples-to-apples with
|
|
49
56
|
the single-threaded libraries) and with its **Web Worker** pool, so the worker
|
|
50
57
|
overhead is never hidden.
|
|
@@ -54,26 +61,31 @@ should not trust anyone's benchmark (including this one) without reproducing it.
|
|
|
54
61
|
Level-6 DEFLATE, one entry (or one batch) compressed in the main thread. This is the
|
|
55
62
|
apples-to-apples comparison against the single-threaded libraries.
|
|
56
63
|
|
|
64
|
+
Time, with the size each library produced — the two are only meaningful together.
|
|
65
|
+
|
|
57
66
|
| Workload | @zip.js/zip.js | jszip | fflate | archiver |
|
|
58
67
|
|---|--:|--:|--:|--:|
|
|
59
|
-
| Compressible text (20 MB) |
|
|
60
|
-
| Incompressible data (20 MB) |
|
|
61
|
-
| Already-compressed media (20 MB) |
|
|
62
|
-
| 5,000 files × ~2 KB |
|
|
68
|
+
| Compressible text (20 MB) | 703 ms / 6.1 MB | 1714 ms / 6.2 MB | 908 ms / 6.3 MB | **702 ms** / 6.1 MB |
|
|
69
|
+
| Incompressible data (20 MB) | 364 ms / 21.0 MB | 858 ms / 21.0 MB | **298 ms** / 21.0 MB | 357 ms / 21.0 MB |
|
|
70
|
+
| Already-compressed media (20 MB) | 362 ms / 21.0 MB | 860 ms / 21.0 MB | **297 ms** / 21.0 MB | 356 ms / 21.0 MB |
|
|
71
|
+
| 5,000 files × ~2 KB | 834 ms / 4.9 MB | 826 ms / 4.7 MB | **305 ms** / 4.8 MB | 431 ms / 4.8 MB |
|
|
63
72
|
|
|
64
73
|
Peak memory for the same runs (Δ over baseline):
|
|
65
74
|
|
|
66
75
|
| Workload | @zip.js/zip.js | jszip | fflate | archiver |
|
|
67
76
|
|---|--:|--:|--:|--:|
|
|
68
|
-
| Compressible text (20 MB) |
|
|
69
|
-
| Incompressible data (20 MB) |
|
|
70
|
-
| Already-compressed media (20 MB) |
|
|
71
|
-
| 5,000 files × ~2 KB |
|
|
72
|
-
|
|
73
|
-
zip.js
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
| Compressible text (20 MB) | 90 MB | 69 MB | **62 MB** | 72 MB |
|
|
78
|
+
| Incompressible data (20 MB) | 153 MB | 92 MB | 108 MB | **81 MB** |
|
|
79
|
+
| Already-compressed media (20 MB) | 154 MB | 93 MB | 108 MB | **74 MB** |
|
|
80
|
+
| 5,000 files × ~2 KB | 252 MB | 267 MB | **102 MB** | 137 MB |
|
|
81
|
+
|
|
82
|
+
On compressible text zip.js and archiver are tied (703 ms against 702 ms, inside the
|
|
83
|
+
noise) and both produce the smallest archive; fflate is 29 % slower here *and* 3 % larger,
|
|
84
|
+
which is the level-6-is-not-a-unit effect described above. On incompressible data — where
|
|
85
|
+
no codec has anything to do — fflate wins on both axes. On 5,000 tiny files fflate is 2.7×
|
|
86
|
+
faster than zip.js and uses a third of the memory; that gap is per-entry orchestration
|
|
87
|
+
(worker round trips, header writes, one stream per entry), not codec speed, as the
|
|
88
|
+
[Codecs](#codecs-compared-at-equal-output-size) section shows.
|
|
77
89
|
|
|
78
90
|
## Parallelism & codec backends — 8 files × 8 MB, single process
|
|
79
91
|
|
|
@@ -82,26 +94,29 @@ in a **plain Node process** (no worker threads unless noted). zip.js can select
|
|
|
82
94
|
codec backend at runtime — native `CompressionStream`, the bundled WebAssembly zlib, or
|
|
83
95
|
a pure-JavaScript zlib port — and it can issue `add()` calls concurrently.
|
|
84
96
|
|
|
85
|
-
| Configuration | Median time | vs jszip |
|
|
86
|
-
|
|
87
|
-
| **zip.js — `CompressionStream`, concurrent `add()`** | **
|
|
88
|
-
| fflate — async (its own worker pool) |
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
| fflate — `zipSync` (single thread) |
|
|
92
|
-
| zip.js — WASM zlib |
|
|
93
|
-
| zip.js — pure-JS zlib |
|
|
94
|
-
| jszip (pako, single thread) |
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
| Configuration | Median time | Output | vs jszip |
|
|
98
|
+
|---|--:|--:|--:|
|
|
99
|
+
| **zip.js — `CompressionStream`, concurrent `add()`** | **607 ms** | 19.6 MB | **9.3×** |
|
|
100
|
+
| fflate — async (its own worker pool) | 733 ms | 20.0 MB | 7.7× |
|
|
101
|
+
| zip.js — `CompressionStream`, sequential | 2214 ms | 19.6 MB | 2.6× |
|
|
102
|
+
| archiver — Node zlib (libuv threadpool) | 2278 ms | 19.6 MB | 2.5× |
|
|
103
|
+
| fflate — `zipSync` (single thread) | 3118 ms | 20.0 MB | 1.8× |
|
|
104
|
+
| zip.js — WASM zlib | 3358 ms | 19.7 MB | 1.7× |
|
|
105
|
+
| zip.js — pure-JS zlib | 3840 ms | 19.7 MB | 1.5× |
|
|
106
|
+
| jszip (pako, single thread) | 5671 ms | 19.7 MB | 1.0× |
|
|
107
|
+
|
|
108
|
+
The two fflate rows produce 20.0 MB where every other row produces 19.6–19.7 MB, i.e. ~2 %
|
|
109
|
+
more, so they are not doing quite the same work as the rows above and below them.
|
|
110
|
+
|
|
111
|
+
**The point:** zip.js with the native `CompressionStream` goes from **2214 ms
|
|
112
|
+
sequential to 607 ms with concurrent `add()` — a 3.6× speedup — using no Web Workers at
|
|
98
113
|
all.** The native codec runs on the platform's threadpool, so independent entries
|
|
99
114
|
compress on multiple cores while your code stays on the main thread. That makes it the
|
|
100
|
-
fastest configuration measured,
|
|
115
|
+
fastest configuration measured, ahead of fflate's dedicated worker pool.
|
|
101
116
|
|
|
102
117
|
One honest caveat, visible in the table: **only the native `CompressionStream` backend
|
|
103
118
|
parallelizes this way.** The WASM and pure-JS backends run synchronously on the main
|
|
104
|
-
thread, so concurrent `add()` does not speed them up (
|
|
119
|
+
thread, so concurrent `add()` does not speed them up (3358 ms and 3840 ms whether
|
|
105
120
|
sequential or "parallel"). Use those backends when a native `CompressionStream` is
|
|
106
121
|
unavailable or when you need byte-identical zlib output; use the native backend when you
|
|
107
122
|
want this parallelism.
|
|
@@ -112,6 +127,11 @@ level (e.g. `{ level: 5 }`) makes zip.js fall back to the WASM zlib codec — wh
|
|
|
112
127
|
the table, does not parallelize via concurrent `add()`. Keep the default level to keep
|
|
113
128
|
the native-backend parallelism, or pair a custom level with Web Workers (below).
|
|
114
129
|
|
|
130
|
+
That fallback costs more than a backend switch, and the
|
|
131
|
+
[frontier](#the-frontier--one-axis-both-sides) measures how much: how much speed a lower
|
|
132
|
+
level actually buys depends on how fast the host's own zlib is, and on Deno it buys
|
|
133
|
+
nothing at all — `level: 5` there is slower *and* larger than the default.
|
|
134
|
+
|
|
115
135
|
### Parallelism is runtime-dependent
|
|
116
136
|
|
|
117
137
|
The table above is measured on **Node**, and its "no Web Workers needed" result does
|
|
@@ -141,6 +161,50 @@ Safari/WebKit, also set `useWebWorkers: true`. Web Workers are the portable way
|
|
|
141
161
|
this parallelism on any runtime — and the only way once you use a non-default level
|
|
142
162
|
(which switches to the WASM codec).
|
|
143
163
|
|
|
164
|
+
## Codecs, compared at equal output size
|
|
165
|
+
|
|
166
|
+
The tables above compare *libraries* — a whole zip pipeline, at each library's default
|
|
167
|
+
"level 6". This one compares only the **codecs**, on the same 20 MB buffer with no zip
|
|
168
|
+
container around them, and sorts by output size rather than by level. That matters because
|
|
169
|
+
zlib's level 6 and fflate's level 6 are different parameter sets: matched by level, the
|
|
170
|
+
comparison silently reads a ratio difference as a speed difference.
|
|
171
|
+
|
|
172
|
+
`frontier` marks a row that nothing smaller beats on time.
|
|
173
|
+
|
|
174
|
+
| Codec | Setting | Output | Ratio | Time | Throughput | |
|
|
175
|
+
|---|---|--:|--:|--:|--:|---|
|
|
176
|
+
| WASM zlib | level 8 | 6,115,980 | 3.429 | 1466 ms | 13.6 MB/s | frontier |
|
|
177
|
+
| `CompressionStream` | no level control | 6,120,503 | 3.426 | 692 ms | 28.9 MB/s | frontier |
|
|
178
|
+
| WASM zlib | level 6 | 6,165,103 | 3.402 | 1023 ms | 19.5 MB/s | |
|
|
179
|
+
| fflate | level 8 — its best | 6,239,025 | 3.361 | 750 ms | 26.7 MB/s | |
|
|
180
|
+
| fflate | level 6 | 6,257,881 | 3.351 | 738 ms | 27.1 MB/s | |
|
|
181
|
+
| WASM zlib | level 5 | 6,562,182 | 3.196 | 494 ms | 40.5 MB/s | frontier |
|
|
182
|
+
| fflate | level 5 | 6,648,574 | 3.154 | 534 ms | 37.4 MB/s | |
|
|
183
|
+
| WASM zlib | level 4 | 6,907,987 | 3.036 | 276 ms | 72.4 MB/s | frontier |
|
|
184
|
+
| fflate | level 1 | 7,261,700 | 2.888 | 316 ms | 63.3 MB/s | |
|
|
185
|
+
| WASM zlib | level 1 | 7,531,042 | 2.785 | 177 ms | 113.1 MB/s | frontier |
|
|
186
|
+
|
|
187
|
+
The pure-JS port produces byte-identical output to the WASM codec at every level and is
|
|
188
|
+
1.2–2× slower; the full 28-row table is in `benchmarks/results/codecs-results.json`.
|
|
189
|
+
|
|
190
|
+
Two things to take from it. **fflate never lands on the frontier here** — at every size it
|
|
191
|
+
reaches, one of zip.js's codecs gets there sooner — and above ratio 3.361 it has no setting
|
|
192
|
+
at all, while the WASM codec keeps going to 3.429. Second, the level numbers really are
|
|
193
|
+
incomparable: fflate's level 6 falls between zlib's levels 5 and 6 on ratio, which is
|
|
194
|
+
exactly why the library tables above show it as both faster *and* larger.
|
|
195
|
+
|
|
196
|
+
Decompression of the same stream:
|
|
197
|
+
|
|
198
|
+
| Codec | Time | Throughput |
|
|
199
|
+
|---|--:|--:|
|
|
200
|
+
| WASM zlib | **46 ms** | 434.7 MB/s |
|
|
201
|
+
| `DecompressionStream` | 56 ms | 354.6 MB/s |
|
|
202
|
+
| pure-JS zlib | 110 ms | 181.5 MB/s |
|
|
203
|
+
| fflate | 123 ms | 162.7 MB/s |
|
|
204
|
+
|
|
205
|
+
The WASM inflate is 2.7× faster than fflate's, and faster than Node's own
|
|
206
|
+
`DecompressionStream`, since the Chromium `inffast_chunk` port landed in 2.11.0.
|
|
207
|
+
|
|
144
208
|
## Decompression
|
|
145
209
|
|
|
146
210
|
Level-6 archives, read back and fully materialized. archiver has no unzip API, so it is
|
|
@@ -148,8 +212,8 @@ excluded.
|
|
|
148
212
|
|
|
149
213
|
| Workload | @zip.js/zip.js | jszip | fflate |
|
|
150
214
|
|---|--:|--:|--:|
|
|
151
|
-
| Compressible text (20 MB) | **
|
|
152
|
-
| 5,000 files × ~2 KB |
|
|
215
|
+
| Compressible text (20 MB) | **66 ms** | 133 ms | 117 ms |
|
|
216
|
+
| 5,000 files × ~2 KB | 603 ms | 445 ms | **104 ms** |
|
|
153
217
|
|
|
154
218
|
zip.js has the fastest large-stream decompression. On thousands of tiny entries the
|
|
155
219
|
per-entry setup cost dominates and **fflate is dramatically faster and lighter** — again
|
|
@@ -160,17 +224,17 @@ the right tool when you are unpacking many small files.
|
|
|
160
224
|
The input is streamed from disk and the archive is streamed back to disk; neither is
|
|
161
225
|
ever fully held in memory (for the libraries that support it).
|
|
162
226
|
|
|
163
|
-
| Library | Median time | Peak memory |
|
|
164
|
-
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
| zip.js (1 thread) |
|
|
168
|
-
| fflate |
|
|
169
|
-
| jszip |
|
|
170
|
-
|
|
171
|
-
zip.js, fflate and archiver all hold memory **flat** while streaming — zip.js peaks
|
|
172
|
-
|
|
173
|
-
compression pipeline. **jszip buffers the entire file** and needs ~
|
|
227
|
+
| Library | Median time | Peak memory (Δ) | Output |
|
|
228
|
+
|---|--:|--:|--:|
|
|
229
|
+
| zip.js (workers) | **8888 ms** | 60 MB | 78.3 MB |
|
|
230
|
+
| archiver | 9150 ms | 71 MB | 78.3 MB |
|
|
231
|
+
| zip.js (1 thread) | 9296 ms | 62 MB | 78.3 MB |
|
|
232
|
+
| fflate | 12549 ms | **43 MB** | 80.2 MB |
|
|
233
|
+
| jszip | 22014 ms | 484 MB | 78.9 MB |
|
|
234
|
+
|
|
235
|
+
zip.js, fflate and archiver all hold memory **flat** while streaming — zip.js peaks ~60 MB
|
|
236
|
+
over baseline regardless of the 256 MB input, thanks to real backpressure through the
|
|
237
|
+
compression pipeline. **jszip buffers the entire file** and needs ~484 MB, at more than
|
|
174
238
|
twice the wall-clock time. If you process files that do not fit comfortably in memory,
|
|
175
239
|
avoid jszip.
|
|
176
240
|
|
|
@@ -178,54 +242,159 @@ avoid jszip.
|
|
|
178
242
|
|
|
179
243
|
How far is zip.js from a native archiver? [`benchmarks/bench-7z.js`](benchmarks/bench-7z.js)
|
|
180
244
|
compares it against the `7zz` CLI (7-Zip 25.01), disk-to-disk on both sides, under Deno
|
|
181
|
-
2.9.
|
|
182
|
-
timings include process spawn (~ms).
|
|
183
|
-
|
|
184
|
-
One comparison is impossible to make perfectly fair: **no 7-Zip preset runs zlib's
|
|
185
|
-
algorithm.** `-mx=5+` is a near-optimal parser (much slower, smaller output) and
|
|
186
|
-
`-mx=1..4` a greedy one (faster, larger output) — they bracket zlib level 6. So the
|
|
187
|
-
table carries two anchors: `-mx=6` as the *ratio* anchor and `-mx=1` as the *speed*
|
|
188
|
-
anchor. On this machine the `-mx` ladder on the 256 MB file reads: `-mx=1/3` → 5.9 s,
|
|
189
|
-
`-mx=5/6` → 34 s, `-mx=7` → 85 s — the 6× jump at `-mx=5` is the switch from greedy
|
|
190
|
-
matching to optimal parsing, and it cannot be hidden by threads (one file = one deflate
|
|
191
|
-
stream = one core).
|
|
192
|
-
|
|
193
|
-
| Workload (compress) | zip.js (workers) | 7-Zip `-mx=6` (mt) | 7-Zip `-mx=1` (mt) |
|
|
194
|
-
|---|--:|--:|--:|
|
|
195
|
-
| Compressible text (20 MB) | **426 ms** / 6.2 MB | 2606 ms / 5.8 MB | 470 ms / 6.8 MB |
|
|
196
|
-
| Incompressible data (20 MB) | **342 ms** | 410 ms | 372 ms |
|
|
197
|
-
| 5,000 files × ~2 KB | 780 ms / 5.5 MB | 199 ms / 4.9 MB | **168 ms** / 5.0 MB |
|
|
198
|
-
| Large file, disk-to-disk (256 MB) | **5.5 s** / 79.5 MB | 33.4 s / 73.6 MB | 5.9 s / 86.7 MB |
|
|
245
|
+
2.9.6. 7-Zip timings include process spawn (~ms).
|
|
199
246
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
247
|
+
**No 7-Zip preset runs zlib's algorithm**, so `-mx=6` and level 6 are not the same request
|
|
248
|
+
and a table pairing them by digit is measuring two things at once. This section therefore
|
|
249
|
+
has two parts: the defaults against each other, and then a frontier that varies *one* axis,
|
|
250
|
+
the compression level, on **both** sides at a fixed core budget.
|
|
251
|
+
|
|
252
|
+
### Defaults against defaults
|
|
253
|
+
|
|
254
|
+
Level 6 and `-mx=6`, i.e. what each tool does when you do not tune it. Every contender the
|
|
255
|
+
harness measures is listed; time / output size.
|
|
256
|
+
|
|
257
|
+
| Workload (compress) | zip.js (1 thread) | zip.js (workers) | 7-Zip (1 thread) | 7-Zip `-mx=6` (mt) | 7-Zip `-mx=1` (mt) |
|
|
258
|
+
|---|--:|--:|--:|--:|--:|
|
|
259
|
+
| Compressible text (20 MB) | **418 ms** / 6.2 MB | 428 ms / 6.2 MB | — | 2540 ms / 5.8 MB | 444 ms / 6.8 MB |
|
|
260
|
+
| Incompressible data (20 MB) | **321 ms** / 21.0 MB | 333 ms / 21.0 MB | — | 418 ms / 21.0 MB | 353 ms / 21.0 MB |
|
|
261
|
+
| 8 files × 8 MB | 1308 ms / 19.9 MB | 308 ms / 19.9 MB | 8102 ms / 18.4 MB | 1510 ms / 18.4 MB | **294 ms** / 21.7 MB |
|
|
262
|
+
| 5,000 files × ~2 KB | 1245 ms / 5.2 MB | 825 ms / 5.2 MB | 580 ms / 4.9 MB | 193 ms / 4.9 MB | **173 ms** / 5.0 MB |
|
|
263
|
+
| Large file, disk-to-disk (256 MB) | **5.4 s** / 79.5 MB | 5.5 s / 79.5 MB | — | 33.4 s / 73.6 MB | 5.8 s / 86.7 MB |
|
|
264
|
+
|
|
265
|
+
| Workload (decompress) | zip.js (1 thread) | zip.js (workers) | 7-Zip (1 thread) | 7-Zip (mt) |
|
|
266
|
+
|---|--:|--:|--:|--:|
|
|
267
|
+
| Compressible text (20 MB) | 83 ms | **61 ms** | — | 81 ms |
|
|
268
|
+
| 8 files × 8 MB | 181 ms | **83 ms** | 241 ms | 247 ms |
|
|
269
|
+
| 5,000 files × ~2 KB | 989 ms | 906 ms | 491 ms | **462 ms** |
|
|
270
|
+
|
|
271
|
+
`7-Zip (1 thread)` is only listed where it means something: a single file is one deflate
|
|
272
|
+
stream, so `-mmt` changes nothing there and the two 7-Zip columns would measure the same
|
|
273
|
+
run. That is also why zip.js's worker pool does nothing on the single-file rows — the
|
|
274
|
+
parallelism both tools have is *between* entries, never inside one.
|
|
275
|
+
|
|
276
|
+
### The frontier — one axis, both sides
|
|
277
|
+
|
|
278
|
+
Levels 1–9 on the zip.js side against `-mx=1,3,5,6,7,9` on the 7-Zip side, sorted by output
|
|
279
|
+
size. A tool is faster than another only where it is faster **at the same size**, and
|
|
280
|
+
`frontier` marks a row that nothing smaller beats on time.
|
|
281
|
+
|
|
282
|
+
**One thread, one deflate stream on both sides** — 20 MB of text:
|
|
283
|
+
|
|
284
|
+
| Encoder | Output | Ratio | Time | |
|
|
285
|
+
|---|--:|--:|--:|---|
|
|
286
|
+
| 7-Zip `-mx=9` | 5.7 MB | 3.665 | 14889 ms | frontier |
|
|
287
|
+
| 7-Zip `-mx=7` | 5.7 MB | 3.665 | 6643 ms | frontier |
|
|
288
|
+
| 7-Zip `-mx=5` | 5.8 MB | 3.646 | 2597 ms | frontier |
|
|
289
|
+
| 7-Zip `-mx=6` | 5.8 MB | 3.646 | 2605 ms | |
|
|
290
|
+
| zip.js level 8 | 6.1 MB | 3.429 | 1537 ms | frontier |
|
|
291
|
+
| zip.js level 9 | 6.1 MB | 3.429 | 1544 ms | |
|
|
292
|
+
| zip.js level 7 | 6.1 MB | 3.421 | 1305 ms | frontier |
|
|
293
|
+
| **zip.js level 6 (default)** | 6.2 MB | 3.377 | **418 ms** | frontier |
|
|
294
|
+
| zip.js level 5 | 6.6 MB | 3.196 | 590 ms | |
|
|
295
|
+
| 7-Zip `-mx=1` | 6.8 MB | 3.096 | 458 ms | |
|
|
296
|
+
| 7-Zip `-mx=3` | 6.8 MB | 3.096 | 453 ms | |
|
|
297
|
+
| zip.js level 4 | 6.9 MB | 3.036 | 358 ms | frontier |
|
|
298
|
+
| zip.js level 3 | 7.0 MB | 2.975 | 420 ms | |
|
|
299
|
+
| zip.js level 2 | 7.4 MB | 2.850 | 266 ms | frontier |
|
|
300
|
+
| zip.js level 1 | 7.5 MB | 2.785 | 239 ms | frontier |
|
|
301
|
+
|
|
302
|
+
**The same 20 MB, one thread, but on Node** — because the zip.js side of that table is not
|
|
303
|
+
one codec. Level 6 is the host's `CompressionStream`, so it changes with the runtime; every
|
|
304
|
+
other level is the bundled WASM codec, which does not (its output is byte-identical on both
|
|
305
|
+
and its times agree within 5 %):
|
|
306
|
+
|
|
307
|
+
| Encoder | Output | Ratio | Time | |
|
|
308
|
+
|---|--:|--:|--:|---|
|
|
309
|
+
| 7-Zip `-mx=9` | 5.7 MB | 3.665 | 14904 ms | frontier |
|
|
310
|
+
| 7-Zip `-mx=7` | 5.7 MB | 3.665 | 6548 ms | frontier |
|
|
311
|
+
| 7-Zip `-mx=5` | 5.8 MB | 3.646 | 2535 ms | frontier |
|
|
312
|
+
| 7-Zip `-mx=6` | 5.8 MB | 3.646 | 2553 ms | |
|
|
313
|
+
| zip.js level 8 | 6.1 MB | 3.429 | 1474 ms | frontier |
|
|
314
|
+
| zip.js level 9 | 6.1 MB | 3.429 | 1476 ms | |
|
|
315
|
+
| **zip.js level 6 (default)** | 6.1 MB | 3.426 | **696 ms** | frontier |
|
|
316
|
+
| zip.js level 7 | 6.1 MB | 3.421 | 1238 ms | |
|
|
317
|
+
| zip.js level 5 | 6.6 MB | 3.196 | 514 ms | frontier |
|
|
318
|
+
| 7-Zip `-mx=1` | 6.8 MB | 3.096 | 447 ms | frontier |
|
|
319
|
+
| 7-Zip `-mx=3` | 6.8 MB | 3.096 | 445 ms | frontier |
|
|
320
|
+
| zip.js level 4 | 6.9 MB | 3.036 | 304 ms | frontier |
|
|
321
|
+
| zip.js level 3 | 7.0 MB | 2.975 | 375 ms | |
|
|
322
|
+
| zip.js level 2 | 7.4 MB | 2.850 | 221 ms | frontier |
|
|
323
|
+
| zip.js level 1 | 7.5 MB | 2.785 | 200 ms | frontier |
|
|
324
|
+
|
|
325
|
+
**Every core, both sides** — 8 files × 8 MB, on Deno. It has to be Deno: Node exposes no
|
|
326
|
+
global `Worker`, so `useWebWorkers` cannot spawn one there and only the default level —
|
|
327
|
+
which rides the platform threadpool instead — parallelizes at all. Measured on Node the
|
|
328
|
+
same table reads 617 ms at level 6 against 4090 ms at level 7, i.e. the WASM levels run
|
|
329
|
+
serially, which would make it a comparison of 7-Zip on 8 cores against zip.js on 1.
|
|
330
|
+
|
|
331
|
+
| Encoder | Output | Ratio | Time | |
|
|
332
|
+
|---|--:|--:|--:|---|
|
|
333
|
+
| 7-Zip `-mx=9` | 18.3 MB | 3.660 | 8625 ms | frontier |
|
|
334
|
+
| 7-Zip `-mx=7` | 18.3 MB | 3.660 | 3796 ms | frontier |
|
|
335
|
+
| 7-Zip `-mx=5` | 18.4 MB | 3.645 | 1510 ms | frontier |
|
|
336
|
+
| 7-Zip `-mx=6` | 18.4 MB | 3.645 | 1540 ms | |
|
|
337
|
+
| zip.js level 9 | 19.6 MB | 3.428 | 971 ms | frontier |
|
|
338
|
+
| zip.js level 8 | 19.6 MB | 3.428 | 991 ms | |
|
|
339
|
+
| zip.js level 7 | 19.6 MB | 3.420 | 852 ms | frontier |
|
|
340
|
+
| **zip.js level 6 (default)** | 19.9 MB | 3.376 | **317 ms** | frontier |
|
|
341
|
+
| zip.js level 5 | 21.0 MB | 3.196 | 405 ms | |
|
|
342
|
+
| 7-Zip `-mx=3` | 21.7 MB | 3.096 | 309 ms | frontier |
|
|
343
|
+
| 7-Zip `-mx=1` | 21.7 MB | 3.096 | 313 ms | |
|
|
344
|
+
| zip.js level 4 | 22.1 MB | 3.037 | 287 ms | frontier |
|
|
345
|
+
| zip.js level 3 | 22.6 MB | 2.975 | 316 ms | |
|
|
346
|
+
| zip.js level 2 | 23.6 MB | 2.849 | 239 ms | frontier |
|
|
347
|
+
| zip.js level 1 | 24.1 MB | 2.784 | 218 ms | frontier |
|
|
348
|
+
|
|
349
|
+
What the two curves say:
|
|
350
|
+
|
|
351
|
+
- **zip.js's default is a spike on its own curve, because it is a different codec.** The
|
|
352
|
+
platform's `CompressionStream` exposes no level control, so **level 6 runs the host's
|
|
353
|
+
native zlib and every other level drops onto the bundled WASM one**. How big the spike is
|
|
354
|
+
therefore depends on whose zlib the host ships: 418 ms at ratio 3.377 on Deno's zlib-ng,
|
|
355
|
+
696 ms at 3.426 on Node's Chromium zlib — ~1.7× faster and ~1.4 % looser.
|
|
356
|
+
- **So whether a lower level buys anything is runtime-dependent, and on Deno it does not.**
|
|
357
|
+
On Node, `level: 5` costs 514 ms against the default's 696 ms: the ordinary trade, 26 %
|
|
358
|
+
faster for 7 % more bytes. On Deno the default is already 418 ms, so the same `level: 5`
|
|
359
|
+
takes 590 ms and produces a *larger* archive — **slower and bigger, strictly worse**. If
|
|
360
|
+
you reach for a lower level to go faster, measure it on your runtime first.
|
|
361
|
+
- **One level is a bad deal everywhere: `level: 3` loses to `level: 4`** on both time and
|
|
362
|
+
size (375 ms / 7.0 MB against 304 ms / 6.9 MB on Node; 420 ms against 358 ms on Deno).
|
|
363
|
+
Both are the same WASM codec, so this one is zlib's own curve, not a fallback artifact.
|
|
364
|
+
- **7-Zip's nine presets are three encoders.** `-mx=1` and `-mx=3` are identical, so are
|
|
365
|
+
`-mx=5`/`-mx=6` and `-mx=7`/`-mx=9` — **`-mx=9` costs 2.3× the time of `-mx=7` for
|
|
366
|
+
byte-identical output**. The 5.7× jump from `-mx=3` to `-mx=5` is greedy matching giving
|
|
367
|
+
way to optimal parsing, and threads cannot hide it.
|
|
368
|
+
- **Against the greedy tier zip.js wins outright.** Single-threaded it is 418 ms / 6.2 MB
|
|
369
|
+
against 453–458 ms / 6.8 MB: faster *and* 9 % smaller than both `-mx=1` and `-mx=3`. On
|
|
370
|
+
all cores it ties them on time (317 ms against 309 ms) and is still 8 % smaller.
|
|
371
|
+
- **Against the optimal-parsing tier it has nothing**, at any level. 7-Zip's cheapest route
|
|
372
|
+
to 3.6 costs 1510 ms where zip.js's best is 971 ms at 3.43; buying that last 6 % of ratio
|
|
373
|
+
costs 4.8× the default's time. That is a real limit of DEFLATE-as-zlib-writes-it, not a
|
|
374
|
+
tuning gap.
|
|
375
|
+
- **Threading buys the two sides different amounts**: 4.2× for zip.js on 8 × 8 MB
|
|
376
|
+
(1308 → 308 ms) against 5.4× for 7-Zip (8102 → 1510 ms). zip.js closes most of that gap
|
|
377
|
+
because it starts from a much faster single-threaded number.
|
|
378
|
+
- **7-Zip legitimately dominates many small files** (~4× on compress, ~2× on decompress):
|
|
379
|
+
its per-entry cost is near zero, while zip.js pays per-entry orchestration. Same lesson
|
|
380
|
+
as the fflate rows above — tiny-entry workloads are zip.js's cost center, and the codec
|
|
381
|
+
is irrelevant there. Note the reverse on 8 × 8 MB decompression, where entries are large
|
|
382
|
+
enough for the pool to pay off: 83 ms against 247 ms, zip.js ~3× faster.
|
|
215
383
|
|
|
216
384
|
## The runtime's zlib decides zip.js throughput
|
|
217
385
|
|
|
218
386
|
On bulk data zip.js is a thin wrapper around the platform's `CompressionStream` — it
|
|
219
387
|
adds ~7 % over the raw encoder on the 256 MB stream. That means throughput is decided
|
|
220
388
|
by **which zlib the runtime vendors**, and they differ a lot. Same 256 MB compressible
|
|
221
|
-
file, one thread, level-6-class output everywhere
|
|
389
|
+
file, one thread, level-6-class output everywhere. This is the one table on this page
|
|
390
|
+
still carrying its July 2026 measurement: it compares runtimes to each other rather than
|
|
391
|
+
zip.js to anything, so it ages with their zlib and not with ours.
|
|
222
392
|
|
|
223
393
|
| Encoder | Time | Output |
|
|
224
394
|
|---|--:|--:|
|
|
225
395
|
| zlib-ng — Deno & Bun `CompressionStream` | **4.8–5.1 s** | 79.4 MB |
|
|
226
396
|
| Chromium zlib — Node `CompressionStream` / `node:zlib` | 8.9 s | 78.3 MB |
|
|
227
397
|
| classic zlib — Apple `gzip -6` | 12.5 s | 78.9 MB |
|
|
228
|
-
| classic zlib compiled to WASM — zip.js `useCompressionStream: false` | ~16 s | 78.9 MB |
|
|
229
398
|
| (reference) 7-Zip `-mx=6` | 33.5 s | 73.6 MB |
|
|
230
399
|
|
|
231
400
|
Verified in the runtimes' sources: Deno builds `flate2` with vendored
|
|
@@ -239,11 +408,13 @@ Consequences worth knowing:
|
|
|
239
408
|
- **The same zip.js code runs ~1.75× faster on Deno/Bun than on Node** for bulk
|
|
240
409
|
compression. A "zip.js is fast/slow" measurement is often really a statement about
|
|
241
410
|
the host's zlib — and the Node tables above are the *pessimistic* end.
|
|
242
|
-
- **The WASM backend is
|
|
243
|
-
WebAssembly overhead
|
|
244
|
-
|
|
245
|
-
against
|
|
246
|
-
(
|
|
411
|
+
- **The WASM backend is a fallback, but a much closer one since 2.11.0.** It used to be
|
|
412
|
+
classic zlib plus WebAssembly overhead, ~3× slower than native on compress. It now
|
|
413
|
+
carries Chromium's zlib: on the codec table above it deflates at level 6 in 1023 ms
|
|
414
|
+
against the native `CompressionStream`'s 692 ms — 1.5× slower for 0.7 % more bytes — and
|
|
415
|
+
it *inflates faster* than the native `DecompressionStream` (46 ms against 56 ms). Reach
|
|
416
|
+
for it when no native `CompressionStream` exists, when you need a specific level, or
|
|
417
|
+
when you need output that does not vary with the host's zlib.
|
|
247
418
|
- Output sizes across the zlib family are interchangeable (78–79 MB): it is one
|
|
248
419
|
algorithm at four levels of implementation tuning. zip.js inherits whichever the
|
|
249
420
|
host provides — including future upgrades, for free.
|
|
@@ -255,8 +426,11 @@ Consequences worth knowing:
|
|
|
255
426
|
low-memory streaming of huge files, and the broadest ZIP feature set in one library —
|
|
256
427
|
AES & ZipCrypto encryption, Zip64, split/multi-volume archives, and an optional Web
|
|
257
428
|
Worker pool.
|
|
258
|
-
- **Choose fflate**
|
|
259
|
-
|
|
429
|
+
- **Choose fflate** for archives of thousands of tiny entries, and when you want the
|
|
430
|
+
smallest memory footprint and the smallest bundle. Note what it does *not* buy you: at
|
|
431
|
+
equal output size its codec is not the fastest one here (see
|
|
432
|
+
[Codecs](#codecs-compared-at-equal-output-size)), so the win is fflate's very low
|
|
433
|
+
per-entry cost, not its deflate.
|
|
260
434
|
- **archiver** is a solid streaming compressor on Node but cannot read archives.
|
|
261
435
|
- **jszip** is convenient but the slowest here and buffers whole files in memory.
|
|
262
436
|
|
|
@@ -271,6 +445,7 @@ npm install # jszip, fflate, archiver (zip.js is used from the repo)
|
|
|
271
445
|
npm run corpus # generate the deterministic datasets under .corpus/
|
|
272
446
|
node bench.js # the head-to-head tables (compress / decompress / disk streaming)
|
|
273
447
|
node bench-backends.js # the parallelism & codec-backend matrix
|
|
448
|
+
node bench-codecs.js # the codecs alone, sorted by output size
|
|
274
449
|
deno run -A bench-7z.js # zip.js vs the 7zz CLI (also: bun bench-7z.js)
|
|
275
450
|
```
|
|
276
451
|
|