@zip.js/zip.js 2.19.0 → 2.21.0

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 CHANGED
@@ -1,15 +1,17 @@
1
1
  # Benchmarks
2
2
 
3
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
4
+ [jszip](https://github.com/Stuk/jszip), [fflate](https://github.com/101arrowz/fflate),
5
+ [archiver](https://github.com/archiverjs/node-archiver) and the
6
+ [ZIP API of `node:zlib`](https://nodejs.org/api/zlib.html#class-zlibzipentry), of zip.js's own
7
+ codec backends, and of
6
8
  the runtimes it runs on. Every number on this page comes from the harness in
7
9
  [`benchmarks/`](benchmarks/), on the machine, date and versions below. The results are specific
8
10
  to them; run the harness on your machine before relying on any of them.
9
11
 
10
12
  The page answers four questions, each from one or two scripts of the harness:
11
13
 
12
- - [How does zip.js compare with jszip, fflate and archiver?](#zipjs-against-jszip-fflate-and-archiver)
14
+ - [How does zip.js compare with jszip, fflate, archiver and node:zlib?](#zipjs-against-jszip-fflate-archiver-and-nodezlib)
13
15
  `bench.js`, on Node.
14
16
  - [Which codec backend of zip.js is fastest, and what does concurrent `add()` buy?](#the-codec-backends-of-zipjs)
15
17
  `bench-codecs.js` and `bench-backends.js`, on Node.
@@ -23,13 +25,14 @@ The page answers four questions, each from one or two scripts of the harness:
23
25
  |---|---|
24
26
  | Machine | Apple M2, 8 cores (4 performance + 4 efficiency), 16 GB RAM |
25
27
  | OS | macOS 27.0 (arm64) |
26
- | Runtimes | Node.js v26.7.0, Bun 1.4.2, Deno 2.9.7 |
28
+ | Runtimes | Node.js v26.10.0, Bun 1.4.2, Deno 2.9.7 |
27
29
  | Browsers | Firefox 156, Chrome 153, headless (the browser encryption table only) |
28
- | zip.js | 2.19.0, measured at 96084cc8; the later commits of the release change no codec or stream path |
30
+ | zip.js | 2.19.0 |
29
31
  | jszip | 3.10.2 |
30
32
  | fflate | 0.8.3 |
31
33
  | archiver | 8.0.0 |
32
- | Measured | 2026-09-26 |
34
+ | node:zlib | the ZIP API built into Node.js 26.10.0, experimental, added in 26.8.0 |
35
+ | Measured | 2026-09-30; the browser encryption table on 2026-09-26 |
33
36
 
34
37
  ## Method
35
38
 
@@ -51,29 +54,37 @@ The page answers four questions, each from one or two scripts of the harness:
51
54
  table; it is not a verdict.
52
55
  - **Memory.** Peak resident set size reported by `/usr/bin/time -l`, as a delta over an empty
53
56
  Node process (about 50 MB). It is the highest of the 3 runs, while the time is their median.
54
- - **Level.** Every library compresses at its own level 6. A level is not a unit shared between
57
+ - **Versions of this page.** A re-run replaces the whole page, and the runtimes, browsers and OS
58
+ move between two runs along with zip.js, so a number read from an older version of the page
59
+ is not comparable with one here: only a column measured in the same run isolates a single
60
+ variable, as the `chunkSize` 64 KB column of the
61
+ [runtime table](#concurrent-add-per-runtime) does for the default chunk size.
62
+ - **Level.** Every library compresses at its own level 6; the ZIP API of `node:zlib` has no level
63
+ option and deflates at zlib's default, which is 6. A level is not a unit shared between
55
64
  libraries: zlib's level 6 and fflate's level 6 are different parameter sets and produce
56
65
  different sizes, so every time is printed next to the size it achieved, and the
57
66
  [Codecs](#codecs-at-equal-output-size) table compares by output size instead.
58
- - **Codecs.** Every head-to-head table names the codec of each row. archiver runs Node's `zlib`
59
- module, the host's zlib written in C; jszip (pako) and fflate deflate and inflate in
60
- JavaScript. zip.js selects its codec at runtime, so it has one row per backend: the
67
+ - **Codecs.** Every head-to-head table names the codec of each row. archiver and the ZIP API of
68
+ `node:zlib` run Node's `zlib` module, the host's zlib written in C; jszip (pako) and fflate
69
+ deflate and inflate in JavaScript. zip.js selects its codec at runtime, so it has one row per backend: the
61
70
  `CompressionStream` and `DecompressionStream` of the runtime, Node's zlib here and the default
62
71
  where they exist; the bundled WebAssembly zlib; and the pure-JavaScript zlib port. The last two
63
72
  ship with the library, like the codecs of jszip and fflate, so those rows compare the
64
73
  libraries without the host's zlib in the picture.
65
74
  - **Checks.** No decompression row verifies the CRC-32 of the entries: zip.js and jszip leave
66
- the check off by default, and fflate has none on read. Turning it on in zip.js makes the
75
+ the check off by default, fflate has none on read, and `node:zlib` verifies by default so its
76
+ row passes `verify: false`. Turning it on in zip.js makes the
67
77
  inflater verify the CRC-32 through a gzip trailer: about 7 ms per 20 MB on the WebAssembly
68
78
  codec, within the noise on Node's `DecompressionStream`; the pure-JavaScript port keeps a
69
79
  separate pass over the output, about 15 ms.
70
80
  - **Units.** MB in the tables is 10^6 bytes. The workload sizes (20 MB, 8 MB, 256 MB) and the
71
81
  MB/s throughputs use 2^20 bytes.
72
82
 
73
- ## zip.js against jszip, fflate and archiver
83
+ ## zip.js against jszip, fflate, archiver and node:zlib
74
84
 
75
85
  One Node process per measurement (`benchmarks/bench.js`), one thread, every library at its
76
- level 6. zip.js appears once per codec backend.
86
+ level 6. zip.js appears once per codec backend. The `node:zlib` rows use the ZIP API built into
87
+ Node.js since 26.8.0: `ZipEntry.create()` and `createZipArchive()` to write, `ZipBuffer` to read.
77
88
 
78
89
  ### Compression
79
90
 
@@ -82,33 +93,37 @@ size each row produced:
82
93
 
83
94
  | Library | Codec | Compressible text (20 MB) | Incompressible data (20 MB) | 5,000 files × ~2 KB |
84
95
  |---|---|--:|--:|--:|
85
- | @zip.js/zip.js | `CompressionStream` | **698 ms** / 6.1 MB | 350 ms / 21.0 MB | 864 ms / 4.9 MB |
86
- | @zip.js/zip.js | WASM zlib | 1049 ms / 6.2 MB | 461 ms / 21.0 MB | 666 ms / 4.9 MB |
87
- | @zip.js/zip.js | pure-JS zlib | 1218 ms / 6.2 MB | 817 ms / 21.0 MB | 1107 ms / 4.9 MB |
88
- | jszip | pako (JavaScript) | 1702 ms / 6.2 MB | 855 ms / 21.0 MB | 834 ms / 4.7 MB |
89
- | fflate | fflate (JavaScript) | 908 ms / 6.3 MB | **296 ms** / 21.0 MB | **276 ms** / 4.8 MB |
90
- | archiver | Node `zlib` (C) | 702 ms / 6.1 MB | 355 ms / 21.0 MB | 421 ms / 4.8 MB |
96
+ | @zip.js/zip.js | `CompressionStream` | 730 ms / 6.1 MB | 357 ms / 21.0 MB | 731 ms / 4.9 MB |
97
+ | @zip.js/zip.js | WASM zlib | 1098 ms / 6.2 MB | 460 ms / 21.0 MB | 551 ms / 4.9 MB |
98
+ | @zip.js/zip.js | pure-JS zlib | 1229 ms / 6.2 MB | 787 ms / 21.0 MB | 962 ms / 4.9 MB |
99
+ | jszip | pako (JavaScript) | 1753 ms / 6.2 MB | 854 ms / 21.0 MB | 846 ms / 4.7 MB |
100
+ | fflate | fflate (JavaScript) | 908 ms / 6.3 MB | **298 ms** / 21.0 MB | **278 ms** / 4.8 MB |
101
+ | archiver | Node `zlib` (C) | 708 ms / 6.1 MB | 359 ms / 21.0 MB | 423 ms / 4.8 MB |
102
+ | node:zlib | Node `zlib` (C) | **678 ms** / 6.1 MB | 346 ms / 21.0 MB | 371 ms / 4.7 MB |
91
103
 
92
104
  Peak memory for the same runs (Δ over baseline):
93
105
 
94
106
  | Library | Codec | Compressible text (20 MB) | Incompressible data (20 MB) | 5,000 files × ~2 KB |
95
107
  |---|---|--:|--:|--:|
96
- | @zip.js/zip.js | `CompressionStream` | 108 MB | 160 MB | 251 MB |
97
- | @zip.js/zip.js | WASM zlib | 115 MB | 187 MB | 260 MB |
98
- | @zip.js/zip.js | pure-JS zlib | 113 MB | 167 MB | 281 MB |
99
- | jszip | pako (JavaScript) | 69 MB | 93 MB | 261 MB |
100
- | fflate | fflate (JavaScript) | **62 MB** | 108 MB | **103 MB** |
101
- | archiver | Node `zlib` (C) | 71 MB | **74 MB** | 136 MB |
102
-
103
- On the text file zip.js with `CompressionStream` and archiver take the same time, 698 and
104
- 702 ms, for the same output size: both run Node's zlib. With the codecs that ship with each
105
- library, fflate takes 908 ms for 2 % more bytes, zip.js's WebAssembly zlib 1049 ms, its
106
- pure-JavaScript port 1218 ms and jszip 1702 ms. On incompressible data fflate is the fastest row
107
- and archiver the smallest in memory. On 5,000 small files the WebAssembly backend is 23 % faster
108
- than `CompressionStream`, so a native stream costs more per entry than a WebAssembly one; fflate
109
- takes 41 % of the WebAssembly row's time there against 87 % on the 20 MB file, and the
110
- difference is the work zip.js does per entry, a stream and a header each. The zip.js rows use
111
- the most memory on the two 20 MB files.
108
+ | @zip.js/zip.js | `CompressionStream` | 115 MB | 184 MB | 262 MB |
109
+ | @zip.js/zip.js | WASM zlib | 139 MB | 206 MB | 244 MB |
110
+ | @zip.js/zip.js | pure-JS zlib | 126 MB | 182 MB | 287 MB |
111
+ | jszip | pako (JavaScript) | 76 MB | 102 MB | 269 MB |
112
+ | fflate | fflate (JavaScript) | 68 MB | 114 MB | **106 MB** |
113
+ | archiver | Node `zlib` (C) | 78 MB | 90 MB | 151 MB |
114
+ | node:zlib | Node `zlib` (C) | **51 MB** | **82 MB** | 245 MB |
115
+
116
+ On the text file the three rows that run Node's zlib take the same time within 8 %, for the
117
+ same output size: `node:zlib` 678 ms, archiver 708 ms and zip.js with `CompressionStream`
118
+ 730 ms. With the codecs that ship with each library, fflate takes 908 ms for 2 % more bytes,
119
+ zip.js's WebAssembly zlib 1098 ms, its pure-JavaScript port 1229 ms and jszip 1753 ms. On
120
+ incompressible data fflate is the fastest row and `node:zlib` the smallest in memory. On 5,000
121
+ small files the WebAssembly backend is 25 % faster than `CompressionStream`, so a native stream
122
+ costs more per entry than a WebAssembly one; fflate takes 50 % of the WebAssembly row's time
123
+ there against 83 % on the 20 MB file, and the difference is the work zip.js does per entry, a
124
+ stream and a header each. `node:zlib` takes 371 ms on the same files and archiver 423 ms, both
125
+ on the same zlib as `CompressionStream`. The zip.js rows use the most memory on the two 20 MB
126
+ files, where `node:zlib`, which holds one buffer per entry and no stream, uses the least.
112
127
 
113
128
  ### Decompression
114
129
 
@@ -117,56 +132,59 @@ excluded.
117
132
 
118
133
  | Library | Codec | Compressible text (20 MB) | 5,000 files × ~2 KB |
119
134
  |---|---|--:|--:|
120
- | @zip.js/zip.js | `DecompressionStream` | **61 ms** | 629 ms |
121
- | @zip.js/zip.js | WASM zlib | 72 ms | 474 ms |
122
- | @zip.js/zip.js | pure-JS zlib | 154 ms | 613 ms |
123
- | jszip | pako (JavaScript) | 133 ms | 440 ms |
124
- | fflate | fflate (JavaScript) | 123 ms | **102 ms** |
135
+ | @zip.js/zip.js | `DecompressionStream` | 64 ms | 485 ms |
136
+ | @zip.js/zip.js | WASM zlib | 71 ms | 342 ms |
137
+ | @zip.js/zip.js | pure-JS zlib | 155 ms | 527 ms |
138
+ | jszip | pako (JavaScript) | 140 ms | 438 ms |
139
+ | fflate | fflate (JavaScript) | 116 ms | **101 ms** |
140
+ | node:zlib | Node `zlib` (C) | **60 ms** | 183 ms |
125
141
 
126
142
  Peak memory for the same runs (Δ over baseline):
127
143
 
128
144
  | Library | Codec | Compressible text (20 MB) | 5,000 files × ~2 KB |
129
145
  |---|---|--:|--:|
130
- | @zip.js/zip.js | `DecompressionStream` | 135 MB | 348 MB |
131
- | @zip.js/zip.js | WASM zlib | 165 MB | 453 MB |
132
- | @zip.js/zip.js | pure-JS zlib | 155 MB | 467 MB |
133
- | jszip | pako (JavaScript) | 103 MB | 303 MB |
134
- | fflate | fflate (JavaScript) | **92 MB** | **119 MB** |
135
-
136
- On the 20 MB stream `DecompressionStream` takes 61 ms and the WebAssembly inflate 72 ms, then
137
- fflate 123 ms, jszip 133 ms and the pure-JavaScript port 154 ms. The WebAssembly row includes
138
- the module instantiation a fresh process pays once, about 20 ms. Warm, the WebAssembly inflate
139
- is 24 % faster than `DecompressionStream` in the [Codecs](#codecs-at-equal-output-size) table.
140
- On 5,000 small files the order reverses: fflate takes 102 ms, jszip 440 ms and the three zip.js
141
- rows 474 to 629 ms, with `DecompressionStream` the slowest of them, the same per-entry cost as
142
- in compression. fflate uses the least memory on both workloads, 34 % of the
143
- `DecompressionStream` row on the small files, and the WebAssembly and pure-JavaScript rows peak
144
- higher than `DecompressionStream`. The three zip.js rows allocate the same 800 MB of stream objects over the
145
- 5,000 entries, about 160 KB per entry. The WebAssembly and pure-JavaScript codecs run on the
146
- JavaScript thread, so V8's incremental marking keeps more of that garbage alive between
147
- collections, and that is the difference between the rows.
146
+ | @zip.js/zip.js | `DecompressionStream` | 151 MB | 355 MB |
147
+ | @zip.js/zip.js | WASM zlib | 171 MB | 358 MB |
148
+ | @zip.js/zip.js | pure-JS zlib | 158 MB | 353 MB |
149
+ | jszip | pako (JavaScript) | 103 MB | 284 MB |
150
+ | fflate | fflate (JavaScript) | **97 MB** | **123 MB** |
151
+ | node:zlib | Node `zlib` (C) | 99 MB | 255 MB |
152
+
153
+ On the 20 MB stream `node:zlib` takes 60 ms and `DecompressionStream` 64 ms, the same zlib
154
+ reached two ways, then the WebAssembly inflate 71 ms, fflate 116 ms, jszip 140 ms and the
155
+ pure-JavaScript port 155 ms. The WebAssembly row includes the module instantiation a fresh
156
+ process pays once, about 20 ms. Warm, the WebAssembly inflate is 27 % faster than
157
+ `DecompressionStream` in the [Codecs](#codecs-at-equal-output-size) table. On 5,000 small files
158
+ the order changes: fflate takes 101 ms, `node:zlib` 183 ms, then the WebAssembly row 342 ms,
159
+ jszip 438 ms, `DecompressionStream` 485 ms and the pure-JavaScript port 527 ms, the same
160
+ per-entry cost as in compression. fflate uses the least memory on both workloads, 35 % of the
161
+ `DecompressionStream` row on the small files. The three zip.js rows allocate the same 800 MB of
162
+ stream objects over the 5,000 entries, about 160 KB per entry, and peak within 5 MB of each
163
+ other.
148
164
 
149
165
  ### Streaming a 256 MB file, disk to disk
150
166
 
151
167
  One file read with `fs.createReadStream` in 64 KB chunks and one archive written with
152
168
  `fs.createWriteStream`, one entry, each library's streaming API. zip.js takes Web Streams, so its
153
169
  rows go through Node's `Readable.toWeb` and `Writable.toWeb` bridges; the other libraries take
154
- the Node streams directly.
170
+ the Node streams directly, `node:zlib` through `ZipEntry.createStream()`.
155
171
 
156
172
  | Library | Codec | Median time | Peak memory (Δ) | Output |
157
173
  |---|---|--:|--:|--:|
158
- | @zip.js/zip.js | `CompressionStream` | **8927 ms** | 85 MB | 78.3 MB |
159
- | archiver | Node `zlib` (C) | 9131 ms | 75 MB | 78.3 MB |
160
- | fflate | fflate (JavaScript) | 12394 ms | **42 MB** | 80.2 MB |
161
- | @zip.js/zip.js | WASM zlib | 13320 ms | 122 MB | 78.9 MB |
162
- | @zip.js/zip.js | pure-JS zlib | 15082 ms | 116 MB | 78.9 MB |
163
- | jszip | pako (JavaScript) | 22243 ms | 74 MB | 78.9 MB |
164
-
165
- Every row streams: the peaks stay between 42 MB (fflate) and 122 MB (the WebAssembly zlib)
166
- over baseline on the 256 MB input. zip.js with `CompressionStream` and archiver take the same
167
- time within 3 %; jszip takes 2.5× that. With its WebAssembly zlib zip.js takes 1.5× the
168
- `CompressionStream` time and 1.1× fflate's, for 1.6 % fewer bytes; the pure-JavaScript port
169
- takes 1.2× fflate's time.
174
+ | node:zlib | Node `zlib` (C) | **8905 ms** | **42 MB** | 78.3 MB |
175
+ | @zip.js/zip.js | `CompressionStream` | 8926 ms | 117 MB | 78.3 MB |
176
+ | archiver | Node `zlib` (C) | 9167 ms | 81 MB | 78.3 MB |
177
+ | fflate | fflate (JavaScript) | 12781 ms | 57 MB | 80.2 MB |
178
+ | @zip.js/zip.js | WASM zlib | 13405 ms | 128 MB | 78.9 MB |
179
+ | @zip.js/zip.js | pure-JS zlib | 15326 ms | 123 MB | 78.9 MB |
180
+ | jszip | pako (JavaScript) | 22809 ms | 83 MB | 78.9 MB |
181
+
182
+ Every row streams: the peaks stay between 42 MB (`node:zlib`) and 128 MB (the WebAssembly zlib)
183
+ over baseline on the 256 MB input. The three rows on Node's zlib take the same time within 3 %;
184
+ jszip takes 2.6× that. With its WebAssembly zlib zip.js takes 1.5× the `CompressionStream` time
185
+ and 1.05× fflate's, for 1.6 % fewer bytes; the pure-JavaScript port takes 1.2× fflate's time.
186
+ The 256 KB chunks zip.js holds at each stage of its pipeline are what separates its
187
+ `CompressionStream` row from `node:zlib` in memory, 117 against 42 MB, at the same speed.
170
188
 
171
189
  ## The codec backends of zip.js
172
190
 
@@ -187,20 +205,20 @@ fastest such row. A row with an empty last column is beaten by nothing on the ta
187
205
 
188
206
  | Codec | Setting | Output | Ratio | Time | Throughput | Beaten by |
189
207
  |---|---|--:|--:|--:|--:|---|
190
- | WASM zlib | level 8 | 6,115,980 | 3.429 | 1479 ms | 13.5 MB/s | |
191
- | `CompressionStream` | no level control | 6,120,503 | 3.426 | 693 ms | 28.9 MB/s | |
192
- | WASM zlib | level 6 | 6,165,103 | 3.402 | 1026 ms | 19.5 MB/s | `CompressionStream` |
208
+ | WASM zlib | level 8 | 6,115,980 | 3.429 | 1486 ms | 13.5 MB/s | |
209
+ | `CompressionStream` | no level control | 6,120,503 | 3.426 | 695 ms | 28.8 MB/s | |
210
+ | WASM zlib | level 6 | 6,165,103 | 3.402 | 1025 ms | 19.5 MB/s | `CompressionStream` |
193
211
  | fflate | level 8, its best | 6,239,025 | 3.361 | 755 ms | 26.5 MB/s | `CompressionStream` |
194
- | fflate | level 6 | 6,257,881 | 3.351 | 732 ms | 27.3 MB/s | `CompressionStream` |
195
- | WASM zlib | level 5 | 6,562,182 | 3.196 | 494 ms | 40.5 MB/s | |
196
- | fflate | level 5 | 6,648,574 | 3.154 | 532 ms | 37.6 MB/s | WASM zlib level 5 |
197
- | WASM zlib | level 4 | 6,907,987 | 3.036 | 275 ms | 72.7 MB/s | |
198
- | fflate | level 1 | 7,261,700 | 2.888 | 315 ms | 63.6 MB/s | WASM zlib level 4 |
199
- | WASM zlib | level 2 | 7,359,387 | 2.850 | 195 ms | 102.8 MB/s | |
200
- | WASM zlib | level 1 | 7,531,042 | 2.785 | 171 ms | 116.7 MB/s | |
212
+ | fflate | level 6 | 6,257,881 | 3.351 | 751 ms | 26.6 MB/s | `CompressionStream` |
213
+ | WASM zlib | level 5 | 6,562,182 | 3.196 | 493 ms | 40.6 MB/s | |
214
+ | fflate | level 5 | 6,648,574 | 3.154 | 534 ms | 37.5 MB/s | WASM zlib level 5 |
215
+ | WASM zlib | level 4 | 6,907,987 | 3.036 | 277 ms | 72.3 MB/s | |
216
+ | fflate | level 1 | 7,261,700 | 2.888 | 319 ms | 62.8 MB/s | WASM zlib level 4 |
217
+ | WASM zlib | level 2 | 7,359,387 | 2.850 | 210 ms | 95.0 MB/s | |
218
+ | WASM zlib | level 1 | 7,531,042 | 2.785 | 173 ms | 115.8 MB/s | |
201
219
 
202
220
  The pure-JavaScript port produces the same size as the WebAssembly codec at every level and
203
- takes 1.0 to 1.8× its time, the gap closing at the higher levels; the full 28-row table is in
221
+ takes 1.0 to 1.9× its time, the gap closing at the higher levels; the full 28-row table is in
204
222
  `benchmarks/results/codecs-results.json`.
205
223
 
206
224
  Every fflate row is beaten by a zlib row: by the host's `CompressionStream` at fflate's levels 6
@@ -212,16 +230,16 @@ Decompression of the same stream:
212
230
 
213
231
  | Codec | Time | Throughput |
214
232
  |---|--:|--:|
215
- | WASM zlib | **42 ms** | 479.8 MB/s |
216
- | `DecompressionStream` | 55 ms | 365.1 MB/s |
217
- | pure-JS zlib | 109 ms | 182.7 MB/s |
218
- | fflate | 121 ms | 165.6 MB/s |
233
+ | WASM zlib | **41 ms** | 483.7 MB/s |
234
+ | `DecompressionStream` | 56 ms | 359.1 MB/s |
235
+ | pure-JS zlib | 108 ms | 185.1 MB/s |
236
+ | fflate | 122 ms | 164.4 MB/s |
219
237
 
220
- The WebAssembly inflate is 24 % faster than Node's `DecompressionStream`; fflate's inflate takes
221
- 2.9× the time of the WebAssembly one, the pure-JavaScript port 2.6×.
238
+ The WebAssembly inflate is 27 % faster than Node's `DecompressionStream`; fflate's inflate takes
239
+ 3.0× the time of the WebAssembly one, the pure-JavaScript port 2.6×.
222
240
 
223
- Alone, the WebAssembly backend deflates at level 6 in 1.48× the time of `CompressionStream` for
224
- 0.7 % more bytes and inflates 24 % faster than `DecompressionStream`. zip.js uses it by itself
241
+ Alone, the WebAssembly backend deflates at level 6 in 1.47× the time of `CompressionStream` for
242
+ 0.7 % more bytes and inflates 27 % faster than `DecompressionStream`. zip.js uses it by itself
225
243
  when no `CompressionStream` exists or when a level other than the default is requested;
226
244
  `useCompressionStream: false` selects it on every host, for an output that does not depend on
227
245
  the host's zlib.
@@ -234,22 +252,28 @@ that buys with each backend.
234
252
 
235
253
  | Configuration | Median time | Output | vs jszip |
236
254
  |---|--:|--:|--:|
237
- | **zip.js — `CompressionStream`, concurrent `add()`** | **598 ms** | 19.6 MB | **9.4×** |
238
- | fflate — async (its own worker pool) | 764 ms | 20.0 MB | 7.4× |
239
- | zip.js — `CompressionStream`, sequential `add()` | 2245 ms | 19.6 MB | 2.5× |
240
- | archiver — Node zlib | 2270 ms | 19.6 MB | 2.5× |
241
- | fflate — `zipSync` (single thread) | 3083 ms | 20.0 MB | 1.8× |
242
- | zip.js — WASM zlib, concurrent `add()` | 3369 ms | 19.7 MB | 1.7× |
243
- | zip.js — WASM zlib, sequential `add()` | 3389 ms | 19.7 MB | 1.7× |
244
- | zip.js — pure-JS zlib, concurrent `add()` | 3822 ms | 19.7 MB | 1.5× |
245
- | zip.js — pure-JS zlib, sequential `add()` | 3872 ms | 19.7 MB | 1.5× |
246
- | jszip (pako, single thread) | 5629 ms | 19.7 MB | 1.0× |
247
-
248
- With the host's `CompressionStream`, zip.js goes from 2245 ms with sequential `add()` calls to
249
- 598 ms with concurrent ones, 3.8×, without Web Workers: Node runs `CompressionStream` off the
255
+ | node:zlib — `ZipEntry.create()` for every entry at once | **596 ms** | 19.6 MB | **9.6×** |
256
+ | **zip.js — `CompressionStream`, concurrent `add()`** | **602 ms** | 19.6 MB | **9.5×** |
257
+ | fflate — async (its own worker pool) | 757 ms | 20.0 MB | 7.6× |
258
+ | node:zlib — `ZipEntry.create()` one entry at a time | 2209 ms | 19.6 MB | 2.6× |
259
+ | zip.js — `CompressionStream`, sequential `add()` | 2257 ms | 19.6 MB | 2.5× |
260
+ | archiver — Node zlib | 2278 ms | 19.6 MB | 2.5× |
261
+ | fflate — `zipSync` (single thread) | 3191 ms | 20.0 MB | 1.8× |
262
+ | zip.js — WASM zlib, sequential `add()` | 3367 ms | 19.7 MB | 1.7× |
263
+ | zip.js — WASM zlib, concurrent `add()` | 3402 ms | 19.7 MB | 1.7× |
264
+ | zip.js — pure-JS zlib, sequential `add()` | 3868 ms | 19.7 MB | 1.5× |
265
+ | zip.js — pure-JS zlib, concurrent `add()` | 3930 ms | 19.7 MB | 1.5× |
266
+ | jszip (pako, single thread) | 5719 ms | 19.7 MB | 1.0× |
267
+
268
+ With the host's `CompressionStream`, zip.js goes from 2257 ms with sequential `add()` calls to
269
+ 602 ms with concurrent ones, 3.7×, without Web Workers: Node runs `CompressionStream` off the
250
270
  main thread, so the entries compress on several cores while the JavaScript thread only feeds
251
- them. fflate's async API, which runs its own worker pool, takes 764 ms on the same input; its
252
- output is 2 % larger than the zlib rows, so it does slightly less work. The WebAssembly and
271
+ them. The ZIP API of `node:zlib` gets the same 3.7× from the same threadpool when its
272
+ `ZipEntry.create()` calls are issued at once, 2209 to 596 ms, the same time as zip.js within
273
+ noise; the difference is in memory, not time: `node:zlib` holds every compressed entry until
274
+ `createZipArchive()` serializes them, while zip.js writes each entry as its codec finishes.
275
+ fflate's async API, which runs its own worker pool, takes 757 ms on the same input; its output
276
+ is 2 % larger than the zlib rows, so it does slightly less work. The WebAssembly and
253
277
  pure-JavaScript backends run on the main thread, and concurrent `add()` changes nothing for them.
254
278
 
255
279
  Requesting a non-default level, e.g. `{ level: 5 }`, selects the bundled codec instead of
@@ -271,21 +295,21 @@ differ:
271
295
 
272
296
  | Runtime | sequential `add()` | concurrent `add()` | concurrent `add()`, `chunkSize` 64 KB | concurrent `add()` + `useWebWorkers` |
273
297
  |---|--:|--:|--:|--:|
274
- | Node.js v26.7.0 | 2.23 s | **0.59 s** | 0.59 s | 0.61 s |
275
- | Bun 1.4.2 | 1.20 s | **0.24 s** | 1.20 s | 0.25 s |
276
- | Deno 2.9.7 | 1.39 s | 1.30 s | 1.31 s | **0.34 s** |
298
+ | Node.js v26.10.0 | 2.25 s | **0.62 s** | 0.59 s | 0.59 s |
299
+ | Bun 1.4.2 | 1.19 s | **0.26 s** | 1.20 s | 0.27 s |
300
+ | Deno 2.9.7 | 1.35 s | 1.35 s | 1.39 s | **0.37 s** |
277
301
 
278
302
  - **Node** runs `CompressionStream` off the JavaScript thread, on the libuv threadpool, four
279
- threads by default (`UV_THREADPOOL_SIZE`): concurrent `add()` alone is 3.8× faster than
303
+ threads by default (`UV_THREADPOOL_SIZE`): concurrent `add()` alone is 3.6× faster than
280
304
  sequential, and the chunk size changes nothing. Node has no `Worker` global, so
281
305
  `useWebWorkers` spawns nothing there and the entries run in-process, in the same time.
282
306
  - **Bun** runs `CompressionStream` off the JavaScript thread only for writes larger than 128 KB
283
307
  (its native implementation, since Bun 1.4 in August 2026). With the 256 KB chunks zip.js
284
- writes by default, concurrent `add()` alone is 5.0× faster than sequential; with the 64 KB
308
+ writes by default, concurrent `add()` alone is 4.5× faster than sequential; with the 64 KB
285
309
  chunks that were the default up to version 2.18.2 it gains nothing, and `useWebWorkers: true`
286
- is 4.8× faster.
310
+ is 4.5× faster too.
287
311
  - **Deno** runs `CompressionStream` on the JavaScript thread whatever the write size: concurrent
288
- `add()` alone gains 7 %, `useWebWorkers: true` is 4.1× faster.
312
+ `add()` alone gains nothing, `useWebWorkers: true` is 3.6× faster.
289
313
 
290
314
  ### Compression levels
291
315
 
@@ -295,13 +319,13 @@ column runs the same JavaScript on each engine. One 20 MB text entry, one thread
295
319
 
296
320
  | Runtime | level 6, `CompressionStream` | level 5, WASM zlib | level 5, pure-JS zlib | level 1, WASM zlib |
297
321
  |---|--:|--:|--:|--:|
298
- | Node.js v26.7.0 | 694 ms / 6.12 MB | 509 ms / 6.56 MB | 773 ms / 6.56 MB | 185 ms / 7.53 MB |
299
- | Bun 1.4.2 | 368 ms / 6.20 MB | 459 ms / 6.56 MB | 661 ms / 6.56 MB | 177 ms / 7.53 MB |
300
- | Deno 2.9.7 | 408 ms / 6.20 MB | 574 ms / 6.56 MB | 666 ms / 6.56 MB | 219 ms / 7.53 MB |
322
+ | Node.js v26.10.0 | 691 ms / 6.12 MB | 512 ms / 6.56 MB | 774 ms / 6.56 MB | 186 ms / 7.53 MB |
323
+ | Bun 1.4.2 | 430 ms / 6.20 MB | 465 ms / 6.56 MB | 665 ms / 6.56 MB | 166 ms / 7.53 MB |
324
+ | Deno 2.9.7 | 409 ms / 6.20 MB | 592 ms / 6.56 MB | 682 ms / 6.56 MB | 236 ms / 7.53 MB |
301
325
 
302
- On Node, level 5 on the WebAssembly codec is 27 % faster than the default for 7 % more bytes.
326
+ On Node, level 5 on the WebAssembly codec is 26 % faster than the default for 7 % more bytes.
303
327
  On Bun and Deno the default is already faster than level 5, so level 5 there is slower and
304
- larger; only level 1 buys speed on them, 52 % on Bun and 46 % on Deno, at 21 % more bytes. The
328
+ larger; only level 1 buys speed on them, 61 % on Bun and 42 % on Deno, at 21 % more bytes. The
305
329
  pure-JavaScript port at level 5 takes 1.5× the WebAssembly time on Node, 1.4× on Bun and 1.2×
306
330
  on Deno, and is slower than the default on every runtime. The two bundled codecs produce the
307
331
  same size on every runtime; the level-6 sizes differ because they come from three different
@@ -315,13 +339,13 @@ memory, fed in 256 KB writes as zip.js does; gzip is Apple's, timed as a child p
315
339
 
316
340
  | Runtime | `CompressionStream` alone | zip.js, one entry | Output |
317
341
  |---|--:|--:|--:|
318
- | Node.js v26.7.0 | 8.85 s | 8.91 s | 78.3 MB |
319
- | Bun 1.4.2 | 4.72 s | 4.75 s | 79.4 MB |
320
- | Deno 2.9.7 | 5.05 s | 5.20 s | 79.4 MB |
321
- | Apple `gzip -6`, classic zlib | 12.2 to 12.6 s | | 78.9 MB |
342
+ | Node.js v26.10.0 | 9.06 s | 9.10 s | 78.3 MB |
343
+ | Bun 1.4.2 | 4.74 s | 4.85 s | 79.4 MB |
344
+ | Deno 2.9.7 | 5.22 s | 5.21 s | 79.4 MB |
345
+ | Apple `gzip -6`, classic zlib | 12.4 to 12.5 s | | 78.9 MB |
322
346
 
323
347
  zip.js is within 3 % of the raw stream on every runtime. On this file Bun's `CompressionStream`
324
- takes 53 % of Node's time and Deno's 57 %, for about 1.4 % more bytes; Apple's gzip takes 1.4×
348
+ takes 52 % of Node's time and Deno's 58 %, for about 1.4 % more bytes; Apple's gzip takes 1.4×
325
349
  Node's time. Which zlib each runtime ships is not verified here; the table measures the result.
326
350
 
327
351
  ## Encryption: AES-256 on a stored entry
@@ -335,9 +359,9 @@ one archive written, then read back.
335
359
 
336
360
  | Engine | Node.js | Bun | Deno |
337
361
  |---|--:|--:|--:|
338
- | WebAssembly, linked into the module of the WebAssembly builds | **138 / 139 MB/s** | **122 / 124** | 128 / 128 |
339
- | JavaScript, the fallback of those builds and the engine of the native and core builds | 118 / 107 | 115 / 115 | **145 / 145** |
340
- | 2.13.1, sjcl | 26 / 26 | 28 / 28 | 20 / 19 |
362
+ | WebAssembly, linked into the module of the WebAssembly builds | **134 / 135 MB/s** | **119 / 121** | 124 / 124 |
363
+ | JavaScript, the fallback of those builds and the engine of the native and core builds | 119 / 108 | 115 / 117 | **144 / 133** |
364
+ | 2.13.1, sjcl (measured on 2026-09-26, Node.js v26.7.0) | 26 / 26 | 28 / 28 | 20 / 19 |
341
365
 
342
366
  The same measurement in a browser, through the Web Worker pool, on 32 MB of bytes generated in
343
367
  the page (the corpus is not served to the browser), median of 3 passes
@@ -350,13 +374,13 @@ file):
350
374
  | JavaScript | 79 / 79 | 104 / 104 |
351
375
  | 2.13.1, sjcl | 17 / 17 | 22 / 22 |
352
376
 
353
- - The JavaScript engine is 4.1 to 4.7× sjcl on Node, Bun and in the two browsers, and 7.3 to
354
- 7.6× on Deno, where sjcl was slowest. sjcl ran the cipher 16 bytes
377
+ - The JavaScript engine is 4.1 to 4.7× sjcl on Node, Bun and in the two browsers, and 7.0 to
378
+ 7.2× on Deno, where sjcl was slowest. sjcl ran the cipher 16 bytes
355
379
  at a time through a bit-array layer; the new engine works on typed arrays and is fed whole
356
380
  chunks, with the AES rounds and the SHA-1 steps written out. Every build has it.
357
381
  - The WebAssembly kernel is the same C code on every host, with the same rounds written out:
358
- 122 to 139 MB/s on Node, Bun and Deno, 125 to 138 MB/s in the two browsers. The JavaScript
359
- engine is within 8 % of it on Bun and 23 % on Node, ahead of it on Deno, and 1.3 to 1.6×
382
+ 119 to 135 MB/s on Node, Bun and Deno, 125 to 138 MB/s in the two browsers. The JavaScript
383
+ engine is within 4 % of it on Bun and 20 % on Node, ahead of it on Deno, and 1.3 to 1.6×
360
384
  slower in the browsers. The WebAssembly builds use
361
385
  the kernel whenever their module loads and fall back to the JavaScript engine when it cannot,
362
386
  for instance under a Content Security Policy that does not allow WebAssembly
@@ -380,7 +404,7 @@ The harness lives in [`benchmarks/`](benchmarks/). `bench.js` and `bench-backend
380
404
 
381
405
  ```sh
382
406
  cd benchmarks
383
- npm install # jszip, fflate, archiver (zip.js is used from the repository)
407
+ npm install # jszip, fflate, archiver (zip.js is used from the repository, node:zlib is built in)
384
408
  npm run corpus # generate the datasets under .corpus/
385
409
  node bench.js # the head-to-head tables: compress, decompress, disk streaming
386
410
  node bench-backends.js # the concurrent add() and backends table
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zip-js/zip-js",
3
- "version": "2.19.0",
3
+ "version": "2.21.0",
4
4
  "exports": {
5
5
  ".": "./index.js",
6
6
  "./external": "./lib/zip-fs-external.js",