Skip to content

Benchmarks

StAX-XML benchmarks the package as a pure JavaScript parser. The latest documentation and the v1.0.0-rc3 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:expanded regenerates the release runtime matrix, the 4 GiB StreamReaderSync index-first benchmark, and the converter compiled batch-plan benchmark, plus the 1 GiB writer benchmark artifact.

Parser benchmarks return JavaScript strings and JavaScript objects. They do not count raw-byte traversal as a completed parse. Large-file checks focus on low RSS, staying below JavaScript string-size limits, and pull-style consumption through the stream, event, and converter APIs.

The current release set was refreshed on Node 24.15.0, Bun 1.3.13, and Deno 2.7.13. Runtime rows use a generated 16 MiB fixture, 1 warmup, and 3 measured runs.

RuntimeWorkloadThroughputAverageHeap delta
Node 24.15.0public-sync-full-string96.1 MiB/s166.50 ms24.6 MiB
Node 24.15.0stream-sync-index-full-string105.5 MiB/s151.66 ms23.3 MiB
Bun 1.3.13public-sync-full-string69.2 MiB/s231.12 ms16.8 MiB
Bun 1.3.13stream-sync-index-full-string138.7 MiB/s115.36 ms67.2 MiB
Deno 2.7.13public-sync-full-string93.3 MiB/s171.51 ms27.5 MiB
Deno 2.7.13stream-sync-index-full-string111.2 MiB/s143.94 ms27.8 MiB

The 4 GiB generated stream fixture uses StreamReaderSync over Iterable<Uint8Array[]>, consumes each batch with eventCount plus index accessors, skips warmup, and measures 3 runs.

WorkloadAverageThroughputEventsAverage RSS delta
StreamReaderSync index-first, 4 GiB43.44 s94.29 MiB/s572,662,3144.0 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 remain separate measurement surfaces:

TrackCommandWhat it measures
Parser fixture seriespnpm --dir packages/benchmark bench:sync2 KiB, 4 KiB, 13 MiB, and 98 MiB parser fixtures plus sync builder rows.
Builder small/largepnpm --dir packages/benchmark bench:builder:small and bench:builder:bigXML emission from builder-friendly intermediate data.
Converter compiled batch planpnpm --dir packages/benchmark bench:converter:compiled-batchManual StreamReaderSync projection vs converter auto-lowered and explicit .compile() batch dispatch plans.
Async parser/writerpnpm --dir packages/benchmark bench:asyncAsync parser and async-vs-sync writer API overhead.
Writer corepnpm --dir packages/benchmark bench:writer:coreDirect writer method throughput for compact and pretty output.
1 GiB writerpnpm --dir packages/benchmark bench:writer:1gb:jsonAsync 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 release artifact keeps the 1 GiB writer result under packages/benchmark/results/release/raw/writer-1gb.json.

The converter comparison is part of the pure JavaScript release contract. It uses a manual StreamReaderSync baseline built from batches with eventCount plus index accessors. The converter rows parse the same byte fixture through:

  • schema.parseSync(bytes), which auto-lowers supported schemas to the compiled dispatch batch plan.
  • schema.compile().parseSync(bytes), which makes the same compiled batch plan explicit and reusable.

Both converter rows are verified against the manual projection before timing. The release artifact is packages/benchmark/results/release/converter-compiled-batch-plan.json.

CaseThroughputAverageBooksChecksum
Manual StreamReaderSync projection80.17 MiB/s199.58 ms139,458-1845341048
Converter schema.parseSync(bytes)47.11 MiB/s339.63 ms139,458-1845341048
Converter schema.compile().parseSync(bytes)46.03 MiB/s347.61 ms139,458-1845341048

For large byte input, prefer batch-local index access:

import { StreamEventType, StreamReaderSync } from 'stax-xml';
const reader = new StreamReaderSync(byteBatches);
for (const batch of reader) {
for (let index = 0; index < batch.eventCount; index++) {
if (batch.typeAt(index) === StreamEventType.START_ELEMENT) {
const name = batch.nameAt(index);
const attributeCount = batch.attributeCountAt(index);
for (let attribute = 0; attribute < attributeCount; attribute++) {
const attributeName = batch.attributeNameAt(index, attribute);
const attributeValue = batch.attributeValueAt(index, attribute);
consume(name, attributeName, attributeValue);
}
}
}
}

for (const event of batch) is still supported, but it allocates wrapper event objects. On the canonical 98 MiB fixture, index-first consumption was 730.56 ms with about 39.8 MiB heap delta, while wrapper iteration was 1.28 s with about 482.7 MiB heap delta.