Benchmarks
StAX-XML benchmarks the package as a pure JavaScript parser. The latest
documentation and the v1.0.0 release benchmark set use this same contract:
measure the public JavaScript surfaces that users actually call, with full
JavaScript string and object materialization.
What to Run
Section titled “What to Run”pnpm --dir packages/benchmark bench:syncmeasures synchronous parser and builder paths.pnpm --dir packages/benchmark bench:asyncmeasures async parser and writer paths.pnpm --dir packages/benchmark bench:runtime-matrixcompares the same JavaScript reader workload across Node, Bun, and Deno when those runtimes are installed.pnpm --filter benchmark run release:writer:cross-runtimecompares the file-writing workload across Node/stax-xml, Java/Woodstox, and Rust/quick-xml.pnpm --filter benchmark run release:reader:cross-runtimecompares the same pull-reader workload across Node/stax-xml, Java/Woodstox, and Rust/quick-xml.pnpm --filter benchmark run release:expandedregenerates the full release benchmark set: runtime matrix, 4 GiB stream reader, converter comparison, 1 GiB writer, parser fixture series, maintained npm XML parser comparison, 1 MiB to 4 GiB size series, and writer small/big/async rows.
Measurement Scope
Section titled “Measurement Scope”Parser benchmarks use the public APIs in fragment mode, which is the library default. They return JavaScript strings and JavaScript objects rather than counting raw-byte traversal as a completed parse. Large-file checks report heap and RSS separately, stay below JavaScript string-size limits, and use pull-style consumption through the stream, event, and converter APIs.
Release Results
Section titled “Release Results”The generated environment and runtime versions are shown below. Runtime rows use a generated 16 MiB fixture, 1 warmup, and 3 measured runs.
Benchmark Environment
Section titled “Benchmark Environment”- Generated: 2026-07-18T17:16:41.036Z
- Run ID:
2026-07-18T17-16-41-036Z - CPU: Apple M4 (~2.40 GHz)
- Runtime: node 26.5.0 (arm64-darwin)
--expose-gc - Package manager: pnpm@11.12.0
- Canonical Set: parser 2 KiB / 4 KiB / 13 MiB / 98 MiB, maintained Node XML parser comparison, StreamReaderSync 1 MiB to 4 GiB size series, runtime matrix, converter compiled batch plan, writer small / big / async / 1 GiB
- Source: release aggregation
Resolved package and runtime versions
| Benchmark package | Resolved version | Declared range | Source |
|---|---|---|---|
fast-xml-parser | 5.7.2 | ^5.2.5 | npm |
htmlparser2 | 12.0.0 | ^12.0.0 | npm |
mitata | 1.0.34 | ^1.0.34 | npm |
sax | 1.6.0 | ^1.4.1 | npm |
saxes | 6.0.0 | ^6.0.0 | npm |
saxophone | 0.8.0 | ^0.8.0 | npm |
stax-xml | 1.0.0 | workspace:* | workspace |
txml | 5.2.1 | ^5.1.1 | npm |
xml-stream | 0.4.5 | ^0.4.5 | npm |
xml2js | 0.6.2 | ^0.6.2 | npm |
Parser Fixture Series
Section titled “Parser Fixture Series”These rows compare parser APIs that all produce the same canonical JavaScript record array for each fixture. Full-result parity is checked before timing. Read them in fixture-size order before comparing the separate npm parser and large-input traversal tables.
Small Documents (2 KiB)
Section titled “Small Documents (2 KiB)”Benchmark source: parser-2kb.mjs, release aggregation
| Surface | Average Time | Operations/sec | Heap | Notes |
|---|---|---|---|---|
| EventReaderSync | 176.77 us | 5,657 ops/sec | 64.9 KiB | String event iterator building the same object shape manually |
| StreamReaderSync | 161.26 us | 6,201 ops/sec | 32.6 KiB | Current-token reader avoiding event object allocation |
| Converter | 195.94 us | 5,104 ops/sec | 45.0 KiB | Declarative schema returning the same object shape |
| fast-xml-parser XMLParser | 256.96 us | 3,892 ops/sec | 201.4 KiB | Object parser plus canonical projection |
| txml parse | 7.73 us | 129,348 ops/sec | 3.9 KiB | Object parser plus canonical projection |
| xml2js parseString | 322.96 us | 3,096 ops/sec | 198.2 KiB | Callback object parser plus canonical projection |
Medium Documents (4 KiB)
Section titled “Medium Documents (4 KiB)”Benchmark source: parser-4kb.mjs, release aggregation
| Surface | Average Time | Operations/sec | Heap | Notes |
|---|---|---|---|---|
| EventReaderSync | 284.06 us | 3,520 ops/sec | 132.1 KiB | String event iterator building the same object shape manually |
| StreamReaderSync | 260.87 us | 3,833 ops/sec | 69.6 KiB | Current-token reader avoiding event object allocation |
| Converter | 299.33 us | 3,341 ops/sec | 125.9 KiB | Declarative schema returning the same object shape |
| fast-xml-parser XMLParser | 382.64 us | 2,613 ops/sec | 691.1 KiB | Object parser plus canonical projection |
| txml parse | 17.57 us | 56,914 ops/sec | 2.9 KiB | Object parser plus canonical projection |
| xml2js parseString | 487.47 us | 2,051 ops/sec | 578.1 KiB | Callback object parser plus canonical projection |
Medium-Large Documents (13 MiB)
Section titled “Medium-Large Documents (13 MiB)”Benchmark source: parser-13mb.mjs, release aggregation
| Surface | Average Time | Operations/sec | Heap | Notes |
|---|---|---|---|---|
| EventReaderSync | 122.59 ms | 8.16 ops/sec | 19.1 MiB | String event iterator building the same object shape manually |
| StreamReaderSync | 105.05 ms | 9.52 ops/sec | 17.8 MiB | Current-token reader avoiding event object allocation |
| Converter | 104.16 ms | 9.6 ops/sec | 12.0 MiB | Declarative schema returning the same object shape |
| fast-xml-parser XMLParser | 504.04 ms | 1.98 ops/sec | 134.6 MiB | Object parser plus canonical projection |
| txml parse | 98.82 ms | 10.12 ops/sec | 126.9 MiB | Object parser plus canonical projection |
| xml2js parseString | 468.81 ms | 2.13 ops/sec | 94.9 MiB | Callback object parser plus canonical projection |
Large Documents (98 MiB)
Section titled “Large Documents (98 MiB)”Benchmark source: parser-98mb.mjs, release aggregation
| Surface | Average Time | Operations/sec | Heap | Notes |
|---|---|---|---|---|
| EventReaderSync | 883.15 ms | 1.13 ops/sec | 38.0 MiB | String event iterator building the same object shape manually |
| StreamReaderSync | 709.20 ms | 1.41 ops/sec | 40.5 MiB | Current-token reader avoiding event object allocation |
| Converter | 664.24 ms | 1.51 ops/sec | 32.7 MiB | Declarative schema returning the same object shape |
| fast-xml-parser XMLParser | 3.72 s | 0.27 ops/sec | 951.3 MiB | Object parser plus canonical projection |
| txml parse | 745.66 ms | 1.34 ops/sec | 873.8 MiB | Object parser plus canonical projection |
| xml2js parseString | 3.49 s | 0.29 ops/sec | 645.6 MiB | Callback object parser plus canonical projection |
Maintained Node XML Parsers
Section titled “Maintained Node XML Parsers”This table compares currently installed public npm XML parser packages on the
same books.xml fixture. Object parsers use their natural string-to-object API.
Event parsers fold event names, text, and attributes into a checksum so the
parse work is retained without forcing a fake object model.
Benchmark source: release aggregation, books fixture
| Library | Average Time | Operations/sec | Heap | Notes |
|---|---|---|---|---|
| stax-xml EventReaderSync | 299.24 us | 3,342 ops/sec | 158.0 KiB | Public string event reader with checksum folding |
| stax-xml StreamReaderSync | 278.47 us | 3,591 ops/sec | 101.9 KiB | Public current-token reader with accessor-based checksum folding |
| fast-xml-parser XMLParser | 390.05 us | 2,564 ops/sec | 768.8 KiB | Object parser |
| txml parse | 70.92 us | 14,100 ops/sec | 82.7 KiB | Lightweight object parser |
| xml2js parseString | 505.31 us | 1,979 ops/sec | 603.1 KiB | Callback object parser |
| sax strict event parser | 407.04 us | 2,457 ops/sec | 387.0 KiB | Strict SAX event parser |
| saxes event parser | 268.13 us | 3,730 ops/sec | 92.4 KiB | Maintained SAX-style parser |
| htmlparser2 xmlMode parser | 328.90 us | 3,040 ops/sec | 286.4 KiB | HTML/XML event parser in xmlMode |
Cross-Language Pull Readers
Section titled “Cross-Language Pull Readers”Each timed run reads and parses the same UTF-8 file. Every public pull reader materializes all element names, non-whitespace text, and attribute names/values; event count and checksum parity are required before reporting.
Benchmark source: cross-language reader
| Reader | Median throughput | Median time | Events | Checksum |
|---|---|---|---|---|
| stax-xml StreamReaderSync (v26.5.0) | 91.6 MiB/s | 174.75 ms | 967,965 | 36104832 |
| Woodstox 6.7.0 (Java 25.0.2) | 333.4 MiB/s | 47.99 ms | 967,965 | 36104832 |
| quick-xml 0.40.1 (Rust 1.95.0) | 570.1 MiB/s | 28.07 ms | 967,965 | 36104832 |
StreamReaderSync Size Series
Section titled “StreamReaderSync Size Series”The large-input artifact uses generated byte batches and the public
StreamReaderSync API in fragment mode. It measures parsing past JavaScript
string-size limits without materializing the whole input as one string. Heap and
RSS deltas both use the same post-warmup, post-GC baseline contract.
Benchmark source: release aggregation, 4 GiB harness
| Size | Throughput | Average | Heap delta | RSS delta | Events | Checksum |
|---|---|---|---|---|---|---|
| (1MiB generated chunks) | 48.89 MiB/s | 20.45 ms | 4.1 MiB | 464.0 KiB | 139,818 | 1349859144 |
| (10MiB generated chunks) | 74.35 MiB/s | 134.50 ms | 4.3 MiB | 528.0 KiB | 1,398,106 | 4191398664 |
| (100MiB generated chunks) | 77.50 MiB/s | 1.29 s | 7.4 MiB | 33.2 MiB | 13,981,018 | 2933107464 |
| (1GiB generated chunks) | 77.84 MiB/s | 13.16 s | 27.8 MiB | 67.2 MiB | 143,165,586 | 1039532941 |
| (4GiB generated chunks) | 84.97 MiB/s | 48.21 s | 27.8 MiB | 78.1 MiB | 572,662,314 | 659690454 |
Reader/Writer Throughput Asymmetry
Section titled “Reader/Writer Throughput Asymmetry”The 1 GiB reader and writer rows can look surprising when read side by side. They are intentionally different workloads; use the generated tables rather than hardcoded cross-run throughput values.
The writer is mostly deterministic append work. The caller already knows each
element name, attribute, and text value, so WriterSyncSink validates its own
writer state, encodes known JavaScript strings, and flushes large sequential
chunks to the sink or file descriptor. It does not search arbitrary XML for
delimiters, recover tokens across chunk boundaries, or discover names, text, and
attributes from incoming bytes. That is why the sync file writer can land in the
same broad throughput class as native writers in other runtimes.
That historical reader row is CPU-bound parsing. The 1 GiB fixture uses generated byte batches rather than disk I/O, so storage speed is not the limiter. The reader must scan every byte, classify markup versus text, maintain XML state, build event batches, keep the accessor API stable, and decode/materialize JavaScript strings for names, text, and attributes when the consumer asks for them. The main restriction is pure-JavaScript byte scanning plus UTF-8 span decoding/string materialization under a stable StAX API. Native parsers such as Woodstox or quick-xml can put delimiter search and tokenization in JVM/Rust code with lower-level buffer access.
Native-addon or FFI-style experiments do not remove that public-contract boundary. A Rust, C, or C++ tokenizer can reduce delimiter-search cost, and a lower-level boundary can expose pointers, buffers, or spans more directly. It still cannot hand ordinary JavaScript consumers a ready-made zero-copy StAX event stream with JavaScript strings. Once the benchmark contract requires JavaScript strings and events, V8 heap objects must be created or copied, and that materialization cost dominates the tokenizer-language or boundary choice.
This is the core tradeoff of the pure JavaScript release: bounded memory and portable byte-offset parsing are prioritized, while parser throughput remains limited by JavaScript tokenizer and string-materialization costs.
Runtime Matrix
Section titled “Runtime Matrix”| Runtime | Workload | Throughput | Average | Peak heap | Peak RSS |
|---|---|---|---|---|---|
| Node 26.5.0 | stream-sync-full | 61.8 MiB/s | 258.74 ms | 21.2 MiB | 155.7 MiB |
| Node 26.5.0 | event-sync-full | 53.4 MiB/s | 299.57 ms | 11.8 MiB | 156.0 MiB |
| Bun 1.3.14 | stream-sync-full | 99.0 MiB/s | 161.56 ms | 33.1 MiB | 223.8 MiB |
| Bun 1.3.14 | event-sync-full | 100.6 MiB/s | 159.05 ms | 17.0 MiB | 264.0 MiB |
| Deno 2.9.3 (v8 14.9.207.2-rusty) | stream-sync-full | 62.5 MiB/s | 255.87 ms | 29.0 MiB | 138.4 MiB |
| Deno 2.9.3 (v8 14.9.207.2-rusty) | event-sync-full | 57.5 MiB/s | 278.38 ms | 34.3 MiB | 138.7 MiB |
The 4 GiB generated stream fixture uses StreamReaderSync over
Iterable<Uint8Array[]> in fragment mode, consumes events through current-token
accessors, skips warmup, and measures 3 runs.
| Workload | Average | Throughput | Events | Heap delta | RSS delta |
|---|---|---|---|---|---|
StreamReaderSync current-token, 4 GiB | 48.21 s | 84.97 MiB/s | 572,662,314 | 27.8 MiB | 78.1 MiB |
Other Benchmark Tracks
Section titled “Other Benchmark Tracks”The release matrix is only one part of the benchmark suite. Parser, stream reader, converter, builder, writer, async writer, and large writer sink benchmarks are release-facing measurement surfaces, not optional side notes:
| Track | Command | What it measures |
|---|---|---|
| Parser fixture series | pnpm --dir packages/benchmark bench:sync |
2 KiB, 4 KiB, 13 MiB, and 98 MiB parser fixtures plus sync builder rows. |
| Builder small/large | pnpm --dir packages/benchmark bench:builder:small and bench:builder:big |
XML emission from builder-friendly intermediate data. |
| Converter schema | pnpm --dir packages/benchmark bench:converter:compiled-batch |
Manual StreamReaderSync projection versus the public schema.parseSync(bytes) path. |
| Async parser/writer | pnpm --dir packages/benchmark bench:async |
Async parser and async-vs-sync writer API overhead. |
| Writer core | pnpm --dir packages/benchmark bench:writer:core |
Direct writer method throughput for compact and pretty output. |
| 1 GiB writer | pnpm --dir packages/benchmark bench:writer:1gb:json |
Async writer and sync sink writer over memory and temp-file targets. |
For large XML output, WriterSyncSink remains the recommended synchronous path
when the caller wants incremental writes without retaining the whole XML string.
The generated release data includes the 1 GiB writer result.
Converter API Comparison
Section titled “Converter API Comparison”The converter comparison measures the same byte fixture through:
schema.parseSync(bytes), which automatically compiles and caches supported streaming dispatch plans.
Both rows are verified against the hand-written projection before timing.
Benchmark source: converter-compiled-batch-plan.mjs, release aggregation
| Case | Throughput | Average | Heap delta | RSS delta | Books | Checksum |
|---|---|---|---|---|---|---|
| Manual StreamReaderSync projection | 102.43 MiB/s | 156.21 ms | 57.8 MiB | 3.8 MiB | 139,458 | -1845341048 |
| Converter schema.parseSync(bytes) | 82.49 MiB/s | 193.96 ms | 61.0 MiB | 3.5 MiB | 139,458 | -1845341048 |
Writer Performance
Section titled “Writer Performance”Writer benchmarks keep builder-friendly input normalization outside the timed region so the rows measure XML emission throughput rather than repeated JSON-shape adaptation.
Small Document Building
Section titled “Small Document Building”Benchmark source: writer release aggregation
| Library | Average Time | Operations/sec | Heap | Notes |
|---|---|---|---|---|
| fast-xml-parser builder | 112.06 us | 8,924 ops/sec | 80.9 KiB | fast-xml-parser builder |
| stax-xml writer | 146.35 us | 6,833 ops/sec | 274.4 KiB | Writer API |
| stax-xml writer sync | 8.05 us | 124,170 ops/sec | 1.9 KiB | Sync writer API |
| stax-xml writer sync sink | 9.91 us | 100,922 ops/sec | 3.5 KiB | Sync streaming sink API |
| xml2js builder | 170.51 us | 5,865 ops/sec | 126.0 KiB | xml2js builder |
Large Document Building
Section titled “Large Document Building”Benchmark source: writer release aggregation
| Library | Average Time | Operations/sec | Heap | Notes |
|---|---|---|---|---|
| fast-xml-parser builder (big.json) | 18.53 ms | 53.97 ops/sec | 25.4 MiB | fast-xml-parser builder |
| stax-xml writer sync (big.json) | 13.89 ms | 72.01 ops/sec | 8.9 MiB | Sync writer API |
| stax-xml writer sync sink (big.json) | 10.44 ms | 95.79 ops/sec | 4.1 MiB | Sync streaming sink API |
| stax-xml writer (big.json) | 30.42 ms | 32.88 ops/sec | 8.5 MiB | Writer API |
Async vs Sync Writer Comparison
Section titled “Async vs Sync Writer Comparison”Benchmark source: writer release aggregation
| Element Count | Async Writer | Sync Writer | Sync Sink | Performance Ratio |
|---|---|---|---|---|
| 1K elements | 4.26 ms | 1.98 ms | 2.18 ms | 1.95x faster (sink) |
| 5K elements | 16.69 ms | 6.05 ms | 7.22 ms | 2.31x faster (sink) |
| 10K elements | 30.13 ms | 14.84 ms | 12.33 ms | 2.44x faster (sink) |
1 GiB Writer Comparison
Section titled “1 GiB Writer Comparison”Benchmark source: writer-1gb.mjs, release aggregation
| Target | Average Time | Throughput | Heap | Peak RSS | Written | Records |
|---|---|---|---|---|---|---|
| Async writer + memory WritableStream | 12.64 s | 81.03 MiB/s | 59.5 MiB | 170.4 MiB | 1.00 GiB | 1,164,225 |
| Sync sink writer + memory sink | 4.83 s | 212.17 MiB/s | 69.6 MiB | 179.0 MiB | 1.00 GiB | 1,164,225 |
| Async writer + temp file | 12.91 s | 79.31 MiB/s | 35.5 MiB | 179.5 MiB | 1.00 GiB | 1,164,225 |
| Sync sink writer + temp file | 5.35 s | 191.45 MiB/s | 68.6 MiB | 179.7 MiB | 1.00 GiB | 1,164,225 |
Cross-Language Writer Comparison
Section titled “Cross-Language Writer Comparison”The public writer APIs generate the same compact XML workload and write it to a real file sink. This is separate from the Node-only 1 GiB suite.
The same generator wrote 12,798 records as compact XML to a real file. Each row is the median of three runs.
| Writer | Median throughput |
|---|---|
| stax-xml WriterSyncSink (v26.5.0) | 187.1 MiB/s |
| Woodstox 6.7.0 (Java 25.0.2) | 125.6 MiB/s |
| quick-xml 0.40.1 (Rust 1.95.0) | 528.4 MiB/s |
Do not mix these values directly with the Node-only 1 GiB suite, which uses a different input size, warm-up, sink, and fixture.
Current-Token Reader Consumption
Section titled “Current-Token Reader Consumption”For large byte input, use the public current-token API:
import { StreamReaderSync, XmlEventType } from 'stax-xml';
const reader = new StreamReaderSync(byteChunks);
while (reader.next() !== null) { if (reader.eventType() === XmlEventType.START_ELEMENT) { const name = reader.name(); const attributeCount = reader.attributeCount();
for (let attribute = 0; attribute < attributeCount; attribute++) { const attributeName = reader.attributeName(attribute); const attributeValue = reader.attributeValue(attribute); consume(name, attributeName, attributeValue); } }}The historical candidate measurements on the canonical 98 MiB fixture recorded
cursor consumption at 730.56 ms with about 39.8 MiB heap delta, versus 1.28 s
and about 482.7 MiB for event-object iteration. Treat those numbers as evidence
for the current-token API shape, not current StreamReaderSync release rows.