@zip.js/zip.js 2.13.1 → 2.14.1
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 +349 -416
- package/deno.json +1 -1
- package/dist/zip-core-external.js +761 -945
- package/dist/zip-core-external.min.js +1 -1
- package/dist/zip-core.js +626 -911
- package/dist/zip-core.min.js +1 -1
- package/dist/zip-fs-core-external.js +768 -948
- package/dist/zip-fs-core-external.min.js +1 -1
- package/dist/zip-fs-core.js +633 -914
- package/dist/zip-fs-core.min.js +1 -1
- package/dist/zip-fs-external.js +768 -948
- package/dist/zip-fs-external.min.js +1 -1
- package/dist/zip-fs-native.js +635 -916
- package/dist/zip-fs-native.min.js +1 -1
- package/dist/zip-fs.js +771 -951
- package/dist/zip-fs.min.js +1 -1
- package/dist/zip-legacy.js +628 -913
- package/dist/zip-legacy.min.js +1 -1
- package/dist/zip-module.wasm +0 -0
- package/dist/zip-native.js +628 -913
- package/dist/zip-native.min.js +1 -1
- package/dist/zip-web-worker-native.js +1 -1
- package/dist/zip-web-worker.js +1 -1
- package/dist/zip.js +764 -948
- package/dist/zip.min.js +1 -1
- package/index-native.cjs +635 -916
- package/index-native.min.js +1 -1
- package/index.cjs +771 -951
- package/index.d.cts +52 -12
- package/index.d.ts +52 -12
- package/index.min.js +1 -1
- package/lib/core/codec-worker.js +37 -26
- package/lib/core/io.js +29 -21
- package/lib/core/streams/aes-crypto-stream.js +58 -99
- package/lib/core/streams/codecs/aes-hmac-sha1.js +486 -0
- package/lib/core/streams/zlib-wasm/aes-hmac-sha1-wasm.js +106 -0
- package/lib/core/streams/zlib-wasm/zlib-streams-loader.js +4 -0
- package/lib/core/streams/zlib-wasm/zlib-streams.js +2 -7
- package/lib/core/streams/zlib-wasm/zlib-streams.wasm +0 -0
- package/lib/core/version.js +1 -1
- package/lib/core/web-worker-base.js +15 -9
- package/lib/core/web-worker-inline-native.js +1 -1
- package/lib/core/web-worker-inline-wasm.js +1 -1
- package/lib/core/web-worker-wasm.js +2 -0
- package/lib/core/zip-fs.js +7 -3
- package/lib/core/zip-reader.js +53 -6
- package/lib/core/zip-writer.js +1 -2
- package/lib/core/zlib-streams-inline.js +1 -1
- package/lib/zip-module-wasm-base.js +3 -1
- package/package.json +1 -1
- package/lib/core/streams/codecs/sjcl.js +0 -795
package/BENCHMARKS.md
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
# Benchmarks
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[jszip](https://github.com/Stuk/jszip), [fflate](https://github.com/101arrowz/fflate)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
22
|
-
> faster *and* tighter than 7-Zip's fast mode on single-file streaming; 7-Zip keeps a
|
|
23
|
-
> real edge only on thousands of tiny files.
|
|
3
|
+
Measurements of [@zip.js/zip.js](https://www.npmjs.com/package/@zip.js/zip.js) against
|
|
4
|
+
[jszip](https://github.com/Stuk/jszip), [fflate](https://github.com/101arrowz/fflate) and
|
|
5
|
+
[archiver](https://github.com/archiverjs/node-archiver), of zip.js's own codec backends, and of
|
|
6
|
+
the runtimes it runs on. Every number on this page comes from the harness in
|
|
7
|
+
[`benchmarks/`](benchmarks/), on the machine, date and versions below. The results are specific
|
|
8
|
+
to them; run the harness on your machine before relying on any of them.
|
|
9
|
+
|
|
10
|
+
The page answers four questions, each from one or two scripts of the harness:
|
|
11
|
+
|
|
12
|
+
- [How does zip.js compare with jszip, fflate and archiver?](#zipjs-against-jszip-fflate-and-archiver)
|
|
13
|
+
`bench.js`, on Node.
|
|
14
|
+
- [Which codec backend of zip.js is fastest, and what does concurrent `add()` buy?](#the-codec-backends-of-zipjs)
|
|
15
|
+
`bench-codecs.js` and `bench-backends.js`, on Node.
|
|
16
|
+
- [How do Node, Bun and Deno differ?](#node-bun-and-deno) `bench-runtimes.js`.
|
|
17
|
+
- [How fast is AES encryption?](#encryption-aes-256-on-a-stored-entry) `bench-aes.js` and
|
|
18
|
+
`bench-aes.html`, on the three runtimes and two browsers.
|
|
24
19
|
|
|
25
20
|
## Environment
|
|
26
21
|
|
|
@@ -28,432 +23,370 @@ should not trust anyone's benchmark (including this one) without reproducing it.
|
|
|
28
23
|
|---|---|
|
|
29
24
|
| Machine | Apple M2, 8 cores (4 performance + 4 efficiency), 16 GB RAM |
|
|
30
25
|
| OS | macOS 26.6.2 (arm64) |
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
26
|
+
| Runtimes | Node.js v26.7.0, Bun 1.4.2, Deno 2.9.6 |
|
|
27
|
+
| Browsers | Firefox 154, Chrome 153 (the browser encryption table only) |
|
|
28
|
+
| zip.js | 2.14.0 |
|
|
29
|
+
| jszip | 3.10.2 |
|
|
34
30
|
| fflate | 0.8.3 |
|
|
35
31
|
| archiver | 8.0.0 |
|
|
36
|
-
| Measured | 2026-09-
|
|
32
|
+
| Measured | 2026-09-10; the two encryption tables on 2026-09-12, on the engines that follow 2.14.0 |
|
|
37
33
|
|
|
38
34
|
## Method
|
|
39
35
|
|
|
40
|
-
- **
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- **
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- **
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
36
|
+
- **Workloads.** Synthetic data from a seeded generator (`benchmarks/lib/corpus.js`), so every
|
|
37
|
+
library and every run sees the same bytes. "Text" is English-looking words that deflate
|
|
38
|
+
compresses about 3.4×, "incompressible" is random bytes, "5,000 files" is 5,000 text files of
|
|
39
|
+
about 2 KB each, and the 8 × 8 MB and 256 MB workloads are text.
|
|
40
|
+
- **Isolation.** In `bench.js` and `bench-backends.js`, each (library, operation, workload)
|
|
41
|
+
combination runs in its own freshly spawned Node process under macOS's `/usr/bin/time -l`, so
|
|
42
|
+
the libraries never share a heap and peak memory is per library. The codec, encryption and
|
|
43
|
+
runtime scripts run in-process, with one warmup run before the timed ones.
|
|
44
|
+
- **Timing.** `performance.now()` around the measured operation. Each combination runs 3 times
|
|
45
|
+
(5 in the encryption script) and the tables report the median. Differences of a few percent
|
|
46
|
+
are within run-to-run noise. A bold cell is the best number among the alternatives it is
|
|
47
|
+
compared with, one per column in the head-to-head tables and one per runtime in the runtime
|
|
48
|
+
table; it is not a verdict.
|
|
49
|
+
- **Memory.** Peak resident set size reported by `/usr/bin/time -l`, as a delta over an empty
|
|
50
|
+
Node process (about 50 MB). It is the highest of the 3 runs, while the time is their median.
|
|
51
|
+
- **Level.** Every library compresses at its own level 6. A level is not a unit shared between
|
|
52
|
+
libraries: zlib's level 6 and fflate's level 6 are different parameter sets and produce
|
|
53
|
+
different sizes, so every time is printed next to the size it achieved, and the
|
|
54
|
+
[Codecs](#codecs-at-equal-output-size) table compares by output size instead.
|
|
55
|
+
- **Codecs.** Every head-to-head table names the codec of each row. archiver runs Node's `zlib`
|
|
56
|
+
module, the host's zlib written in C; jszip (pako) and fflate deflate and inflate in
|
|
57
|
+
JavaScript. zip.js selects its codec at runtime, so it has one row per backend: the
|
|
58
|
+
`CompressionStream` and `DecompressionStream` of the runtime, Node's zlib here and the default
|
|
59
|
+
where they exist; the bundled WebAssembly zlib; and the pure-JavaScript zlib port. The last two
|
|
60
|
+
ship with the library, like the codecs of jszip and fflate, so those rows compare the
|
|
61
|
+
libraries without the host's zlib in the picture.
|
|
62
|
+
- **Units.** MB in the tables is 10^6 bytes. The workload sizes (20 MB, 8 MB, 256 MB) and the
|
|
63
|
+
MB/s throughputs use 2^20 bytes.
|
|
64
|
+
|
|
65
|
+
## zip.js against jszip, fflate and archiver
|
|
66
|
+
|
|
67
|
+
One Node process per measurement (`benchmarks/bench.js`), one thread, every library at its
|
|
68
|
+
level 6. zip.js appears once per codec backend.
|
|
69
|
+
|
|
70
|
+
### Compression
|
|
71
|
+
|
|
72
|
+
One entry, or one batch of entries, compressed by a single sequence of calls. Time, with the
|
|
73
|
+
size each row produced:
|
|
74
|
+
|
|
75
|
+
| Library | Codec | Compressible text (20 MB) | Incompressible data (20 MB) | 5,000 files × ~2 KB |
|
|
76
|
+
|---|---|--:|--:|--:|
|
|
77
|
+
| @zip.js/zip.js | `CompressionStream` | **700 ms** / 6.1 MB | 365 ms / 21.0 MB | 888 ms / 4.9 MB |
|
|
78
|
+
| @zip.js/zip.js | WASM zlib | 1057 ms / 6.2 MB | 475 ms / 21.0 MB | 679 ms / 4.9 MB |
|
|
79
|
+
| @zip.js/zip.js | pure-JS zlib | 1238 ms / 6.2 MB | 802 ms / 21.0 MB | 1125 ms / 4.9 MB |
|
|
80
|
+
| jszip | pako (JavaScript) | 1723 ms / 6.2 MB | 862 ms / 21.0 MB | 843 ms / 4.7 MB |
|
|
81
|
+
| fflate | fflate (JavaScript) | 909 ms / 6.3 MB | **297 ms** / 21.0 MB | **281 ms** / 4.8 MB |
|
|
82
|
+
| archiver | Node `zlib` (C) | 702 ms / 6.1 MB | 359 ms / 21.0 MB | 424 ms / 4.8 MB |
|
|
72
83
|
|
|
73
84
|
Peak memory for the same runs (Δ over baseline):
|
|
74
85
|
|
|
75
|
-
|
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
a pure-JavaScript zlib port — and it can issue `add()` calls concurrently.
|
|
86
|
+
| Library | Codec | Compressible text (20 MB) | Incompressible data (20 MB) | 5,000 files × ~2 KB |
|
|
87
|
+
|---|---|--:|--:|--:|
|
|
88
|
+
| @zip.js/zip.js | `CompressionStream` | 91 MB | 153 MB | 245 MB |
|
|
89
|
+
| @zip.js/zip.js | WASM zlib | 119 MB | 170 MB | 250 MB |
|
|
90
|
+
| @zip.js/zip.js | pure-JS zlib | 114 MB | 172 MB | 276 MB |
|
|
91
|
+
| jszip | pako (JavaScript) | 69 MB | 93 MB | 269 MB |
|
|
92
|
+
| fflate | fflate (JavaScript) | **62 MB** | 108 MB | **102 MB** |
|
|
93
|
+
| archiver | Node `zlib` (C) | 72 MB | **74 MB** | 137 MB |
|
|
94
|
+
|
|
95
|
+
On the text file zip.js with `CompressionStream` and archiver take the same time, 700 and
|
|
96
|
+
702 ms, for the same output size: both run Node's zlib. With the codecs that ship with each
|
|
97
|
+
library, fflate takes 909 ms for 2 % more bytes, zip.js's WebAssembly zlib 1057 ms, its
|
|
98
|
+
pure-JavaScript port 1238 ms and jszip 1723 ms. On incompressible data fflate is the fastest row
|
|
99
|
+
and archiver the smallest in memory. On 5,000 small files the WebAssembly backend is 23 % faster
|
|
100
|
+
than `CompressionStream`, so a native stream costs more per entry than a WebAssembly one; fflate
|
|
101
|
+
takes 41 % of the WebAssembly row's time there against 86 % on the 20 MB file, and the
|
|
102
|
+
difference is the work zip.js does per entry, a stream and a header each. The zip.js rows use
|
|
103
|
+
the most memory on the two 20 MB files.
|
|
104
|
+
|
|
105
|
+
### Decompression
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
|
101
|
-
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
| zip.js
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 | |
|
|
107
|
+
Level-6 archives, read back and fully materialized. archiver has no unzip API, so it is
|
|
108
|
+
excluded.
|
|
109
|
+
|
|
110
|
+
| Library | Codec | Compressible text (20 MB) | 5,000 files × ~2 KB |
|
|
111
|
+
|---|---|--:|--:|
|
|
112
|
+
| @zip.js/zip.js | `DecompressionStream` | **66 ms** | 628 ms |
|
|
113
|
+
| @zip.js/zip.js | WASM zlib | 78 ms | 476 ms |
|
|
114
|
+
| @zip.js/zip.js | pure-JS zlib | 180 ms | 615 ms |
|
|
115
|
+
| jszip | pako (JavaScript) | 137 ms | 444 ms |
|
|
116
|
+
| fflate | fflate (JavaScript) | 116 ms | **102 ms** |
|
|
117
|
+
|
|
118
|
+
Peak memory for the same runs (Δ over baseline):
|
|
119
|
+
|
|
120
|
+
| Library | Codec | Compressible text (20 MB) | 5,000 files × ~2 KB |
|
|
121
|
+
|---|---|--:|--:|
|
|
122
|
+
| @zip.js/zip.js | `DecompressionStream` | 116 MB | 350 MB |
|
|
123
|
+
| @zip.js/zip.js | WASM zlib | 163 MB | 484 MB |
|
|
124
|
+
| @zip.js/zip.js | pure-JS zlib | 154 MB | 451 MB |
|
|
125
|
+
| jszip | pako (JavaScript) | 103 MB | 306 MB |
|
|
126
|
+
| fflate | fflate (JavaScript) | **92 MB** | **120 MB** |
|
|
127
|
+
|
|
128
|
+
On the 20 MB stream `DecompressionStream` takes 66 ms and the WebAssembly inflate 78 ms, then
|
|
129
|
+
fflate 116 ms, jszip 137 ms and the pure-JavaScript port 180 ms. On 5,000 small files the order
|
|
130
|
+
reverses: fflate takes 102 ms, jszip 444 ms and the three zip.js rows 476 to 628 ms, with
|
|
131
|
+
`DecompressionStream` the slowest of them, the same per-entry cost as in compression. fflate
|
|
132
|
+
uses the least memory on both workloads, 34 % of the `DecompressionStream` row on the small
|
|
133
|
+
files, and the WebAssembly and pure-JavaScript rows peak higher than `DecompressionStream`. The
|
|
134
|
+
three zip.js rows allocate the same 800 MB of stream objects over the 5,000 entries, about
|
|
135
|
+
160 KB per entry. The WebAssembly and pure-JavaScript codecs run on the JavaScript thread, so
|
|
136
|
+
V8's incremental marking keeps more of that garbage alive between collections, and that is the
|
|
137
|
+
difference between the rows.
|
|
138
|
+
|
|
139
|
+
### Streaming a 256 MB file, disk to disk
|
|
140
|
+
|
|
141
|
+
One file read with `fs.createReadStream` in 64 KB chunks and one archive written with
|
|
142
|
+
`fs.createWriteStream`, one entry, each library's streaming API. zip.js takes Web Streams, so its
|
|
143
|
+
rows go through Node's `Readable.toWeb` and `Writable.toWeb` bridges; the other libraries take
|
|
144
|
+
the Node streams directly.
|
|
145
|
+
|
|
146
|
+
| Library | Codec | Median time | Peak memory (Δ) | Output |
|
|
147
|
+
|---|---|--:|--:|--:|
|
|
148
|
+
| @zip.js/zip.js | `CompressionStream` | **9055 ms** | 62 MB | 78.3 MB |
|
|
149
|
+
| archiver | Node `zlib` (C) | 9115 ms | 71 MB | 78.3 MB |
|
|
150
|
+
| fflate | fflate (JavaScript) | 12456 ms | **41 MB** | 80.2 MB |
|
|
151
|
+
| @zip.js/zip.js | WASM zlib | 13629 ms | 88 MB | 78.9 MB |
|
|
152
|
+
| @zip.js/zip.js | pure-JS zlib | 15485 ms | 120 MB | 78.9 MB |
|
|
153
|
+
| jszip | pako (JavaScript) | 22539 ms | 74 MB | 78.9 MB |
|
|
154
|
+
|
|
155
|
+
Every row streams: the peaks stay between 41 MB (fflate) and 120 MB (the pure-JavaScript port)
|
|
156
|
+
over baseline on the 256 MB input. zip.js with `CompressionStream` and archiver take the same
|
|
157
|
+
time within 1 %; jszip takes 2.5× that. With its WebAssembly zlib zip.js takes 1.5× the
|
|
158
|
+
`CompressionStream` time and 1.1× fflate's, for 1.6 % fewer bytes; the pure-JavaScript port
|
|
159
|
+
takes 1.2× fflate's time.
|
|
160
|
+
|
|
161
|
+
## The codec backends of zip.js
|
|
162
|
+
|
|
163
|
+
zip.js selects its codec at runtime: the `CompressionStream` and `DecompressionStream` of the
|
|
164
|
+
host when they exist and the level is the default, otherwise the bundled WebAssembly zlib, and
|
|
165
|
+
the pure-JavaScript zlib port where WebAssembly cannot load or when it is configured. This
|
|
166
|
+
section measures them on Node, alone and then inside the zip pipeline.
|
|
167
|
+
|
|
168
|
+
### Codecs at equal output size
|
|
169
|
+
|
|
170
|
+
The same 20 MB text buffer with no zip container around it (`benchmarks/bench-codecs.js`),
|
|
171
|
+
sorted by output size rather than by level, because zlib's level 6 and fflate's level 6 are
|
|
172
|
+
different parameter sets: matched by level, the comparison reads a ratio difference as a speed
|
|
173
|
+
difference.
|
|
174
|
+
|
|
175
|
+
A row is beaten when another row is at least as small and faster; the last column names the
|
|
176
|
+
fastest such row. A row with an empty last column is beaten by nothing on the table.
|
|
177
|
+
|
|
178
|
+
| Codec | Setting | Output | Ratio | Time | Throughput | Beaten by |
|
|
175
179
|
|---|---|--:|--:|--:|--:|---|
|
|
176
|
-
| WASM zlib | level 8 | 6,115,980 | 3.429 |
|
|
177
|
-
| `CompressionStream` | no level control | 6,120,503 | 3.426 |
|
|
178
|
-
| WASM zlib | level 6 | 6,165,103 | 3.402 |
|
|
179
|
-
| fflate | level 8
|
|
180
|
-
| fflate | level 6 | 6,257,881 | 3.351 |
|
|
181
|
-
| WASM zlib | level 5 | 6,562,182 | 3.196 |
|
|
182
|
-
| fflate | level 5 | 6,648,574 | 3.154 |
|
|
183
|
-
| WASM zlib | level 4 | 6,907,987 | 3.036 |
|
|
184
|
-
| fflate | level 1 | 7,261,700 | 2.888 |
|
|
185
|
-
| WASM zlib | level
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
180
|
+
| WASM zlib | level 8 | 6,115,980 | 3.429 | 1504 ms | 13.3 MB/s | |
|
|
181
|
+
| `CompressionStream` | no level control | 6,120,503 | 3.426 | 716 ms | 27.9 MB/s | |
|
|
182
|
+
| WASM zlib | level 6 | 6,165,103 | 3.402 | 1047 ms | 19.1 MB/s | `CompressionStream` |
|
|
183
|
+
| fflate | level 8, its best | 6,239,025 | 3.361 | 767 ms | 26.1 MB/s | `CompressionStream` |
|
|
184
|
+
| fflate | level 6 | 6,257,881 | 3.351 | 815 ms | 24.6 MB/s | `CompressionStream` |
|
|
185
|
+
| WASM zlib | level 5 | 6,562,182 | 3.196 | 505 ms | 39.6 MB/s | |
|
|
186
|
+
| fflate | level 5 | 6,648,574 | 3.154 | 541 ms | 36.9 MB/s | WASM zlib level 5 |
|
|
187
|
+
| WASM zlib | level 4 | 6,907,987 | 3.036 | 278 ms | 71.9 MB/s | |
|
|
188
|
+
| fflate | level 1 | 7,261,700 | 2.888 | 318 ms | 62.9 MB/s | WASM zlib level 4 |
|
|
189
|
+
| WASM zlib | level 2 | 7,359,387 | 2.850 | 195 ms | 102.5 MB/s | |
|
|
190
|
+
| WASM zlib | level 1 | 7,531,042 | 2.785 | 175 ms | 114.1 MB/s | |
|
|
191
|
+
|
|
192
|
+
The pure-JavaScript port produces the same size as the WebAssembly codec at every level and
|
|
193
|
+
takes 1.0 to 1.8× its time, the gap closing at the higher levels; the full 28-row table is in
|
|
194
|
+
`benchmarks/results/codecs-results.json`.
|
|
195
|
+
|
|
196
|
+
Every fflate row is beaten by a zlib row: by the host's `CompressionStream` at fflate's levels 6
|
|
197
|
+
to 9, by the WebAssembly codec below. Above ratio 3.361 fflate has no setting, while the
|
|
198
|
+
WebAssembly codec reaches 3.429. fflate's level 6 sits between zlib's levels 5 and 6 on ratio,
|
|
199
|
+
which is why its output is larger than zip.js's in the library tables.
|
|
195
200
|
|
|
196
201
|
Decompression of the same stream:
|
|
197
202
|
|
|
198
203
|
| Codec | Time | Throughput |
|
|
199
204
|
|---|--:|--:|
|
|
200
|
-
| WASM zlib | **
|
|
201
|
-
| `DecompressionStream` |
|
|
202
|
-
| pure-JS zlib |
|
|
203
|
-
| fflate |
|
|
205
|
+
| WASM zlib | **54 ms** | 369.5 MB/s |
|
|
206
|
+
| `DecompressionStream` | 57 ms | 352.6 MB/s |
|
|
207
|
+
| pure-JS zlib | 111 ms | 180.6 MB/s |
|
|
208
|
+
| fflate | 128 ms | 156.0 MB/s |
|
|
204
209
|
|
|
205
|
-
The
|
|
206
|
-
|
|
210
|
+
The WebAssembly inflate and Node's `DecompressionStream` are within 6 % of each other; fflate's
|
|
211
|
+
inflate takes 2.4× the time of the WebAssembly one, the pure-JavaScript port 2.1×.
|
|
207
212
|
|
|
208
|
-
|
|
213
|
+
Alone, the WebAssembly backend deflates at level 6 in 1.46× the time of `CompressionStream` for
|
|
214
|
+
0.7 % more bytes and inflates within 6 % of `DecompressionStream`. zip.js uses it by itself
|
|
215
|
+
when no `CompressionStream` exists or when a level other than the default is requested;
|
|
216
|
+
`useCompressionStream: false` selects it on every host, for an output that does not depend on
|
|
217
|
+
the host's zlib.
|
|
209
218
|
|
|
210
|
-
|
|
211
|
-
excluded.
|
|
219
|
+
### Concurrent `add()` and the backends
|
|
212
220
|
|
|
213
|
-
|
|
221
|
+
8 files × 8 MB of text, 64 MB in one Node process (`benchmarks/bench-backends.js`), without
|
|
222
|
+
Web Workers unless noted. `add()` calls can be issued concurrently, and the table shows what
|
|
223
|
+
that buys with each backend.
|
|
224
|
+
|
|
225
|
+
| Configuration | Median time | Output | vs jszip |
|
|
214
226
|
|---|--:|--:|--:|
|
|
215
|
-
|
|
|
216
|
-
|
|
|
227
|
+
| **zip.js — `CompressionStream`, concurrent `add()`** | **676 ms** | 19.6 MB | **8.6×** |
|
|
228
|
+
| fflate — async (its own worker pool) | 768 ms | 20.0 MB | 7.5× |
|
|
229
|
+
| archiver — Node zlib | 2324 ms | 19.6 MB | 2.5× |
|
|
230
|
+
| zip.js — `CompressionStream`, sequential `add()` | 2421 ms | 19.6 MB | 2.4× |
|
|
231
|
+
| fflate — `zipSync` (single thread) | 3165 ms | 20.0 MB | 1.8× |
|
|
232
|
+
| zip.js — WASM zlib, sequential `add()` | 3553 ms | 19.7 MB | 1.6× |
|
|
233
|
+
| zip.js — WASM zlib, concurrent `add()` | 3577 ms | 19.7 MB | 1.6× |
|
|
234
|
+
| zip.js — pure-JS zlib, concurrent `add()` | 3954 ms | 19.7 MB | 1.5× |
|
|
235
|
+
| zip.js — pure-JS zlib, sequential `add()` | 4008 ms | 19.7 MB | 1.4× |
|
|
236
|
+
| jszip (pako, single thread) | 5788 ms | 19.7 MB | 1.0× |
|
|
237
|
+
|
|
238
|
+
With the host's `CompressionStream`, zip.js goes from 2421 ms with sequential `add()` calls to
|
|
239
|
+
676 ms with concurrent ones, 3.6×, without Web Workers: Node runs `CompressionStream` off the
|
|
240
|
+
main thread, so the entries compress on several cores while the JavaScript thread only feeds
|
|
241
|
+
them. fflate's async API, which runs its own worker pool, takes 768 ms on the same input; its
|
|
242
|
+
output is 2 % larger than the zlib rows, so it does slightly less work. The WebAssembly and
|
|
243
|
+
pure-JavaScript backends run on the main thread, and concurrent `add()` changes nothing for them.
|
|
244
|
+
|
|
245
|
+
Requesting a non-default level, e.g. `{ level: 5 }`, selects the bundled codec instead of
|
|
246
|
+
`CompressionStream`, which has no level control, and gives up this parallelism unless
|
|
247
|
+
`useWebWorkers` is set. The [Compression levels](#compression-levels) table shows what a lower
|
|
248
|
+
level buys on each runtime.
|
|
249
|
+
|
|
250
|
+
## Node, Bun and Deno
|
|
251
|
+
|
|
252
|
+
The same zip.js code under the three runtimes, in-process, median of 3
|
|
253
|
+
(`benchmarks/bench-runtimes.js`). Browsers were not measured for these tables.
|
|
254
|
+
|
|
255
|
+
### Concurrent `add()` per runtime
|
|
256
|
+
|
|
257
|
+
Concurrent `add()` spreads across cores only where the runtime runs `CompressionStream` off the
|
|
258
|
+
JavaScript thread. Same 8 × 8 MB workload as the backends table, level 6. That table spawns one
|
|
259
|
+
process per run and this one measures in-process after a warmup, which is why their Node numbers
|
|
260
|
+
differ:
|
|
261
|
+
|
|
262
|
+
| Runtime | sequential `add()` | concurrent `add()` | concurrent `add()`, `chunkSize` 256 KB | concurrent `add()` + `useWebWorkers` |
|
|
263
|
+
|---|--:|--:|--:|--:|
|
|
264
|
+
| Node.js v26.7.0 | 2.25 s | **0.59 s** | 0.59 s | 0.59 s |
|
|
265
|
+
| Bun 1.4.2 | 1.19 s | 1.19 s | **0.24 s** | 0.25 s |
|
|
266
|
+
| Deno 2.9.6 | 1.31 s | 1.31 s | 1.33 s | **0.34 s** |
|
|
267
|
+
|
|
268
|
+
- **Node** runs `CompressionStream` off the JavaScript thread, on the libuv threadpool, four
|
|
269
|
+
threads by default (`UV_THREADPOOL_SIZE`): concurrent `add()` alone is 3.8× faster than
|
|
270
|
+
sequential, and the chunk size changes nothing. Node has no `Worker` global, so
|
|
271
|
+
`useWebWorkers` spawns nothing there and the entries run in-process, in the same time.
|
|
272
|
+
- **Bun** runs `CompressionStream` off the JavaScript thread only for writes larger than 128 KB
|
|
273
|
+
(its native implementation, since Bun 1.4 in August 2026). zip.js writes 64 KB chunks by
|
|
274
|
+
default, so concurrent `add()` alone gains nothing on Bun; `chunkSize: 256 * 1024` makes it
|
|
275
|
+
4.9× faster, and `useWebWorkers: true` 4.7×.
|
|
276
|
+
- **Deno** runs `CompressionStream` on the JavaScript thread whatever the write size: concurrent
|
|
277
|
+
`add()` alone gains nothing, `useWebWorkers: true` is 3.9× faster.
|
|
278
|
+
|
|
279
|
+
### Compression levels
|
|
280
|
+
|
|
281
|
+
Level 6 runs the host's `CompressionStream`, every other level the bundled WebAssembly zlib, so
|
|
282
|
+
whether a lower level buys speed depends on how fast the host's zlib is. The pure-JavaScript
|
|
283
|
+
column runs the same JavaScript on each engine. One 20 MB text entry, one thread:
|
|
284
|
+
|
|
285
|
+
| Runtime | level 6, `CompressionStream` | level 5, WASM zlib | level 5, pure-JS zlib | level 1, WASM zlib |
|
|
286
|
+
|---|--:|--:|--:|--:|
|
|
287
|
+
| Node.js v26.7.0 | 686 ms / 6.12 MB | 521 ms / 6.56 MB | 780 ms / 6.56 MB | 192 ms / 7.53 MB |
|
|
288
|
+
| Bun 1.4.2 | 371 ms / 6.20 MB | 464 ms / 6.56 MB | 665 ms / 6.56 MB | 168 ms / 7.53 MB |
|
|
289
|
+
| Deno 2.9.6 | 403 ms / 6.21 MB | 590 ms / 6.56 MB | 689 ms / 6.56 MB | 228 ms / 7.53 MB |
|
|
217
290
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
291
|
+
On Node, level 5 on the WebAssembly codec is 24 % faster than the default for 7 % more bytes.
|
|
292
|
+
On Bun and Deno the default is already faster than level 5, so level 5 there is slower and
|
|
293
|
+
larger; only level 1 buys speed on them, 55 % on Bun and 44 % on Deno, at 21 % more bytes. The
|
|
294
|
+
pure-JavaScript port at level 5 takes 1.5× the WebAssembly time on Node, 1.4× on Bun and 1.2×
|
|
295
|
+
on Deno, and is slower than the default on every runtime. The two bundled codecs produce the
|
|
296
|
+
same size on every runtime; the level-6 sizes differ because they come from three different
|
|
297
|
+
zlib builds.
|
|
221
298
|
|
|
222
|
-
|
|
299
|
+
### One 256 MB stream
|
|
223
300
|
|
|
224
|
-
|
|
225
|
-
|
|
301
|
+
On bulk data zip.js adds almost nothing over the host's `CompressionStream`, so its throughput is
|
|
302
|
+
that of the zlib the runtime ships, and they differ. One 256 MB text file, one thread, in
|
|
303
|
+
memory, fed in 64 KB writes as zip.js does; gzip is Apple's, timed as a child process:
|
|
226
304
|
|
|
227
|
-
|
|
|
305
|
+
| Runtime | `CompressionStream` alone | zip.js, one entry | Output |
|
|
228
306
|
|---|--:|--:|--:|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
the
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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.
|
|
383
|
-
|
|
384
|
-
## The runtime's zlib decides zip.js throughput
|
|
385
|
-
|
|
386
|
-
On bulk data zip.js is a thin wrapper around the platform's `CompressionStream` — it
|
|
387
|
-
adds ~7 % over the raw encoder on the 256 MB stream. That means throughput is decided
|
|
388
|
-
by **which zlib the runtime vendors**, and they differ a lot. Same 256 MB compressible
|
|
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.
|
|
392
|
-
|
|
393
|
-
| Encoder | Time | Output |
|
|
307
|
+
| Node.js v26.7.0 | 8.99 s | 8.78 s | 78.3 MB |
|
|
308
|
+
| Bun 1.4.2 | 4.73 s | 4.77 s | 79.4 MB |
|
|
309
|
+
| Deno 2.9.6 | 5.13 s | 5.23 s | 79.5 MB |
|
|
310
|
+
| Apple `gzip -6`, classic zlib | 12.3 to 12.4 s | | 78.9 MB |
|
|
311
|
+
|
|
312
|
+
zip.js is within 2 % of the raw stream on every runtime. On this file Bun's `CompressionStream`
|
|
313
|
+
takes 53 % of Node's time and Deno's 57 %, for about 1.5 % more bytes; Apple's gzip takes 1.4×
|
|
314
|
+
Node's time. Which zlib each runtime ships is not verified here; the table measures the result.
|
|
315
|
+
|
|
316
|
+
## Encryption: AES-256 on a stored entry
|
|
317
|
+
|
|
318
|
+
An AES entry pays the cipher on every byte, so this section measures the engines zip.js can run
|
|
319
|
+
the WinZip cipher on (AES-CTR with the counter WinZip specifies, HMAC-SHA1) against the sjcl code
|
|
320
|
+
they replaced after 2.13.1. The entry is stored (level 0) so no codec sits in the pipeline, and
|
|
321
|
+
the "before" row is the 2.13.1 bundle measured by the same script. 20 MB of incompressible data,
|
|
322
|
+
in-process, single thread, median of 5 (`benchmarks/bench-aes.js`); the two numbers of a cell are
|
|
323
|
+
one archive written, then read back.
|
|
324
|
+
|
|
325
|
+
| Engine | Node.js | Bun | Deno |
|
|
326
|
+
|---|--:|--:|--:|
|
|
327
|
+
| WebAssembly, linked into the module of the WebAssembly builds | **131 / 130 MB/s** | **118 / 121** | 123 / 122 |
|
|
328
|
+
| JavaScript, the fallback of those builds and the engine of the native and core builds | 114 / 114 | 112 / 111 | **142 / 129** |
|
|
329
|
+
| 2.13.1, sjcl | 25 / 25 | 27 / 27 | 19 / 19 |
|
|
330
|
+
|
|
331
|
+
The same measurement in a browser, through the Web Worker pool, on 32 MB of bytes generated in
|
|
332
|
+
the page (the corpus is not served to the browser), median of 3 passes
|
|
333
|
+
([`benchmarks/bench-aes.html`](benchmarks/bench-aes.html), which prints its result and writes no
|
|
334
|
+
file):
|
|
335
|
+
|
|
336
|
+
| Engine | Firefox 154 | Chrome 153 |
|
|
394
337
|
|---|--:|--:|
|
|
395
|
-
|
|
|
396
|
-
|
|
|
397
|
-
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
## When to pick which
|
|
423
|
-
|
|
424
|
-
- **Choose zip.js** for the fastest compression of large or multiple entries
|
|
425
|
-
(parallelism with no Web Workers), the fastest large-stream decompression, flat
|
|
426
|
-
low-memory streaming of huge files, and the broadest ZIP feature set in one library —
|
|
427
|
-
AES & ZipCrypto encryption, Zip64, split/multi-volume archives, and an optional Web
|
|
428
|
-
Worker pool.
|
|
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.
|
|
434
|
-
- **archiver** is a solid streaming compressor on Node but cannot read archives.
|
|
435
|
-
- **jszip** is convenient but the slowest here and buffers whole files in memory.
|
|
338
|
+
| WebAssembly | **96 / 101 MB/s** | **118 / 121** |
|
|
339
|
+
| JavaScript | 68 / 69 | 100 / 98 |
|
|
340
|
+
| 2.13.1, sjcl | 17 / 17 | 22 / 22 |
|
|
341
|
+
|
|
342
|
+
- The JavaScript engine is 4.0 to 4.6× sjcl on Node, Bun and in the two browsers, and 6.8 to
|
|
343
|
+
7.5× on Deno, where sjcl was slowest. sjcl ran the cipher 16 bytes
|
|
344
|
+
at a time through a bit-array layer; the new engine works on typed arrays and is fed whole
|
|
345
|
+
chunks, with the AES rounds and the SHA-1 steps written out. Every build has it.
|
|
346
|
+
- The WebAssembly kernel is the same C code on every host, with the same rounds written out:
|
|
347
|
+
118 to 131 MB/s on Node, Bun and Deno, 96 to 121 MB/s in the two browsers. The JavaScript
|
|
348
|
+
engine is within 13 % of it on Node and Bun, ahead of it on Deno, and 1.2 to 1.4× slower in
|
|
349
|
+
the browsers. The WebAssembly builds use
|
|
350
|
+
the kernel whenever their module loads and fall back to the JavaScript engine when it cannot,
|
|
351
|
+
for instance under a Content Security Policy that does not allow WebAssembly
|
|
352
|
+
(`'wasm-unsafe-eval'`).
|
|
353
|
+
- Neither is hardware AES. OpenSSL on this machine (`openssl speed -evp aes-256-ctr`, 8 KB
|
|
354
|
+
blocks) runs AES-256-CTR at 9.4 GB/s with the ARMv8 AES instructions and at
|
|
355
|
+
249 MB/s with them disabled (`OPENSSL_armcap=0`). WebAssembly has no access to those
|
|
356
|
+
instructions, and Web Crypto cannot run this counter mode: it increments the last byte of the
|
|
357
|
+
counter where WinZip increments the first, and it exposes no ECB to build the keystream from.
|
|
358
|
+
- The bundles did not grow with the new engines. Gzipped, `dist/zip.min.js` went from 67,869
|
|
359
|
+
bytes in 2.13.1 to 67,820 in 2.14.0, `dist/zip-native.min.js` from 73,798 to 72,060 and
|
|
360
|
+
`dist/zip-core.min.js` from 36,405 to 35,493 (`git show <tag>:<file> | gzip -9 | wc -c`):
|
|
361
|
+
sjcl's removal outweighs the JavaScript engine and the kernel. Writing the rounds out after
|
|
362
|
+
2.14.0 gives some of it back: 71,238, 73,339 and 36,235 bytes for the three files behind the
|
|
363
|
+
encryption tables above.
|
|
436
364
|
|
|
437
365
|
## Reproduce
|
|
438
366
|
|
|
439
|
-
The harness lives in [`benchmarks/`](benchmarks/).
|
|
440
|
-
run
|
|
367
|
+
The harness lives in [`benchmarks/`](benchmarks/). `bench.js` and `bench-backends.js` need macOS
|
|
368
|
+
(`/usr/bin/time -l` for peak memory) and Node; the other scripts run wherever zip.js runs.
|
|
441
369
|
|
|
442
370
|
```sh
|
|
443
371
|
cd benchmarks
|
|
444
|
-
npm install
|
|
445
|
-
npm run corpus
|
|
446
|
-
node bench.js
|
|
447
|
-
node bench-backends.js
|
|
448
|
-
node bench-codecs.js
|
|
449
|
-
|
|
372
|
+
npm install # jszip, fflate, archiver (zip.js is used from the repository)
|
|
373
|
+
npm run corpus # generate the datasets under .corpus/
|
|
374
|
+
node bench.js # the head-to-head tables: compress, decompress, disk streaming
|
|
375
|
+
node bench-backends.js # the concurrent add() and backends table
|
|
376
|
+
node bench-codecs.js # the codecs alone, sorted by output size
|
|
377
|
+
node bench-aes.js # the AES engines; also: bun bench-aes.js, deno run -A bench-aes.js
|
|
378
|
+
node bench-runtimes.js # the runtime tables; also: bun bench-runtimes.js, deno run -A bench-runtimes.js
|
|
450
379
|
```
|
|
451
380
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
`
|
|
458
|
-
|
|
459
|
-
|
|
381
|
+
Each script writes its JSON to `benchmarks/results/`; the `*-run.log` files there are the
|
|
382
|
+
console output of the runs behind this page. `RUNS=<n>` sets the number of repetitions (default
|
|
383
|
+
3, 5 in `bench-aes.js`). `ZIPJS_BUNDLE=<path to a previous index.min.js> node bench-aes.js` adds
|
|
384
|
+
the row of a previous release; `git show v2.13.1:index.min.js` gave the sjcl row. The browser
|
|
385
|
+
encryption table comes from `benchmarks/bench-aes.html`, served from the repository root
|
|
386
|
+
(`npx http-server -p 8080`, then `http://localhost:8080/benchmarks/bench-aes.html`), with
|
|
387
|
+
`?engine=js` for the JavaScript engine and `?bundle=zipjs-2.13.1.min.js` for a bundle copied next
|
|
388
|
+
to the page; it prints the median of its three passes and a JSON line.
|
|
389
|
+
|
|
390
|
+
Two more scripts feed no table on this page: `bench-crc.js` compares the two ways zip.js can
|
|
391
|
+
obtain the CRC-32 of an entry compressed by `CompressionStream`, and `bench-7z.js` compares
|
|
392
|
+
zip.js with the 7-Zip command line (`brew install sevenzip`, runs under Deno or Bun).
|