Skip to content
StAX-XML

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.

  • pnpm --dir packages/benchmark bench:sync measures synchronous parser and builder paths.
  • pnpm --dir packages/benchmark bench:async measures async parser and writer paths.
  • pnpm --dir packages/benchmark bench:runtime-matrix compares the same JavaScript reader workload across Node, Bun, and Deno when those runtimes are installed.
  • pnpm --filter benchmark run release:writer:cross-runtime compares the file-writing workload across Node/stax-xml, Java/Woodstox, and Rust/quick-xml.
  • pnpm --filter benchmark run release:reader:cross-runtime compares the same pull-reader workload across Node/stax-xml, Java/Woodstox, and Rust/quick-xml.
  • pnpm --filter benchmark run release:expanded regenerates 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.

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.

The generated environment and runtime versions are shown below. Runtime rows use a generated 16 MiB fixture, 1 warmup, and 3 measured runs.

  • 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 packageResolved versionDeclared rangeSource
fast-xml-parser5.7.2^5.2.5npm
htmlparser212.0.0^12.0.0npm
mitata1.0.34^1.0.34npm
sax1.6.0^1.4.1npm
saxes6.0.0^6.0.0npm
saxophone0.8.0^0.8.0npm
stax-xml1.0.0workspace:*workspace
txml5.2.1^5.1.1npm
xml-stream0.4.5^0.4.5npm
xml2js0.6.2^0.6.2npm

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.

Benchmark source: parser-2kb.mjs, release aggregation

SurfaceAverage TimeOperations/secHeapNotes
EventReaderSync176.77 us5,657 ops/sec64.9 KiBString event iterator building the same object shape manually
StreamReaderSync161.26 us6,201 ops/sec32.6 KiBCurrent-token reader avoiding event object allocation
Converter195.94 us5,104 ops/sec45.0 KiBDeclarative schema returning the same object shape
fast-xml-parser XMLParser256.96 us3,892 ops/sec201.4 KiBObject parser plus canonical projection
txml parse7.73 us129,348 ops/sec3.9 KiBObject parser plus canonical projection
xml2js parseString322.96 us3,096 ops/sec198.2 KiBCallback object parser plus canonical projection

Benchmark source: parser-4kb.mjs, release aggregation

SurfaceAverage TimeOperations/secHeapNotes
EventReaderSync284.06 us3,520 ops/sec132.1 KiBString event iterator building the same object shape manually
StreamReaderSync260.87 us3,833 ops/sec69.6 KiBCurrent-token reader avoiding event object allocation
Converter299.33 us3,341 ops/sec125.9 KiBDeclarative schema returning the same object shape
fast-xml-parser XMLParser382.64 us2,613 ops/sec691.1 KiBObject parser plus canonical projection
txml parse17.57 us56,914 ops/sec2.9 KiBObject parser plus canonical projection
xml2js parseString487.47 us2,051 ops/sec578.1 KiBCallback object parser plus canonical projection

Benchmark source: parser-13mb.mjs, release aggregation

SurfaceAverage TimeOperations/secHeapNotes
EventReaderSync122.59 ms8.16 ops/sec19.1 MiBString event iterator building the same object shape manually
StreamReaderSync105.05 ms9.52 ops/sec17.8 MiBCurrent-token reader avoiding event object allocation
Converter104.16 ms9.6 ops/sec12.0 MiBDeclarative schema returning the same object shape
fast-xml-parser XMLParser504.04 ms1.98 ops/sec134.6 MiBObject parser plus canonical projection
txml parse98.82 ms10.12 ops/sec126.9 MiBObject parser plus canonical projection
xml2js parseString468.81 ms2.13 ops/sec94.9 MiBCallback object parser plus canonical projection

Benchmark source: parser-98mb.mjs, release aggregation

SurfaceAverage TimeOperations/secHeapNotes
EventReaderSync883.15 ms1.13 ops/sec38.0 MiBString event iterator building the same object shape manually
StreamReaderSync709.20 ms1.41 ops/sec40.5 MiBCurrent-token reader avoiding event object allocation
Converter664.24 ms1.51 ops/sec32.7 MiBDeclarative schema returning the same object shape
fast-xml-parser XMLParser3.72 s0.27 ops/sec951.3 MiBObject parser plus canonical projection
txml parse745.66 ms1.34 ops/sec873.8 MiBObject parser plus canonical projection
xml2js parseString3.49 s0.29 ops/sec645.6 MiBCallback object parser plus canonical projection

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

LibraryAverage TimeOperations/secHeapNotes
stax-xml EventReaderSync299.24 us3,342 ops/sec158.0 KiBPublic string event reader with checksum folding
stax-xml StreamReaderSync278.47 us3,591 ops/sec101.9 KiBPublic current-token reader with accessor-based checksum folding
fast-xml-parser XMLParser390.05 us2,564 ops/sec768.8 KiBObject parser
txml parse70.92 us14,100 ops/sec82.7 KiBLightweight object parser
xml2js parseString505.31 us1,979 ops/sec603.1 KiBCallback object parser
sax strict event parser407.04 us2,457 ops/sec387.0 KiBStrict SAX event parser
saxes event parser268.13 us3,730 ops/sec92.4 KiBMaintained SAX-style parser
htmlparser2 xmlMode parser328.90 us3,040 ops/sec286.4 KiBHTML/XML event parser in xmlMode

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

ReaderMedian throughputMedian timeEventsChecksum
stax-xml StreamReaderSync (v26.5.0)91.6 MiB/s174.75 ms967,96536104832
Woodstox 6.7.0 (Java 25.0.2)333.4 MiB/s47.99 ms967,96536104832
quick-xml 0.40.1 (Rust 1.95.0)570.1 MiB/s28.07 ms967,96536104832

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

SizeThroughputAverageHeap deltaRSS deltaEventsChecksum
(1MiB generated chunks)48.89 MiB/s20.45 ms4.1 MiB464.0 KiB139,8181349859144
(10MiB generated chunks)74.35 MiB/s134.50 ms4.3 MiB528.0 KiB1,398,1064191398664
(100MiB generated chunks)77.50 MiB/s1.29 s7.4 MiB33.2 MiB13,981,0182933107464
(1GiB generated chunks)77.84 MiB/s13.16 s27.8 MiB67.2 MiB143,165,5861039532941
(4GiB generated chunks)84.97 MiB/s48.21 s27.8 MiB78.1 MiB572,662,314659690454

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.

RuntimeWorkloadThroughputAveragePeak heapPeak RSS
Node 26.5.0stream-sync-full61.8 MiB/s258.74 ms21.2 MiB155.7 MiB
Node 26.5.0event-sync-full53.4 MiB/s299.57 ms11.8 MiB156.0 MiB
Bun 1.3.14stream-sync-full99.0 MiB/s161.56 ms33.1 MiB223.8 MiB
Bun 1.3.14event-sync-full100.6 MiB/s159.05 ms17.0 MiB264.0 MiB
Deno 2.9.3 (v8 14.9.207.2-rusty)stream-sync-full62.5 MiB/s255.87 ms29.0 MiB138.4 MiB
Deno 2.9.3 (v8 14.9.207.2-rusty)event-sync-full57.5 MiB/s278.38 ms34.3 MiB138.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.

WorkloadAverageThroughputEventsHeap deltaRSS delta
StreamReaderSync current-token, 4 GiB48.21 s84.97 MiB/s572,662,31427.8 MiB78.1 MiB

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.

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

CaseThroughputAverageHeap deltaRSS deltaBooksChecksum
Manual StreamReaderSync projection102.43 MiB/s156.21 ms57.8 MiB3.8 MiB139,458-1845341048
Converter schema.parseSync(bytes)82.49 MiB/s193.96 ms61.0 MiB3.5 MiB139,458-1845341048

Writer benchmarks keep builder-friendly input normalization outside the timed region so the rows measure XML emission throughput rather than repeated JSON-shape adaptation.

Benchmark source: writer release aggregation

LibraryAverage TimeOperations/secHeapNotes
fast-xml-parser builder112.06 us8,924 ops/sec80.9 KiBfast-xml-parser builder
stax-xml writer146.35 us6,833 ops/sec274.4 KiBWriter API
stax-xml writer sync8.05 us124,170 ops/sec1.9 KiBSync writer API
stax-xml writer sync sink9.91 us100,922 ops/sec3.5 KiBSync streaming sink API
xml2js builder170.51 us5,865 ops/sec126.0 KiBxml2js builder

Benchmark source: writer release aggregation

LibraryAverage TimeOperations/secHeapNotes
fast-xml-parser builder (big.json)18.53 ms53.97 ops/sec25.4 MiBfast-xml-parser builder
stax-xml writer sync (big.json)13.89 ms72.01 ops/sec8.9 MiBSync writer API
stax-xml writer sync sink (big.json)10.44 ms95.79 ops/sec4.1 MiBSync streaming sink API
stax-xml writer (big.json)30.42 ms32.88 ops/sec8.5 MiBWriter API

Benchmark source: writer release aggregation

Element CountAsync WriterSync WriterSync SinkPerformance Ratio
1K elements4.26 ms1.98 ms2.18 ms1.95x faster (sink)
5K elements16.69 ms6.05 ms7.22 ms2.31x faster (sink)
10K elements30.13 ms14.84 ms12.33 ms2.44x faster (sink)

Benchmark source: writer-1gb.mjs, release aggregation

TargetAverage TimeThroughputHeapPeak RSSWrittenRecords
Async writer + memory WritableStream12.64 s81.03 MiB/s59.5 MiB170.4 MiB1.00 GiB1,164,225
Sync sink writer + memory sink4.83 s212.17 MiB/s69.6 MiB179.0 MiB1.00 GiB1,164,225
Async writer + temp file12.91 s79.31 MiB/s35.5 MiB179.5 MiB1.00 GiB1,164,225
Sync sink writer + temp file5.35 s191.45 MiB/s68.6 MiB179.7 MiB1.00 GiB1,164,225

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.

WriterMedian 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.

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.