벤치마크
StAX-XML 벤치마크는 순수 JavaScript 파서를 기준으로 합니다. latest 문서와
v1.0.0 릴리스 결과는 같은 측정 기준을 사용합니다. JavaScript에서 실제
호출하는 공개 API가 문자열과 객체를 만드는 비용까지 포함해 측정합니다.
실행 대상
섹션 제목: “실행 대상”pnpm --dir packages/benchmark bench:sync: 동기 parser와 builder 경로.pnpm --dir packages/benchmark bench:async: 비동기 parser와 writer 경로.pnpm --dir packages/benchmark bench:runtime-matrix: Node, Bun, Deno가 설치되어 있을 때 같은 JavaScript reader 작업을 런타임별로 비교.pnpm --filter benchmark run release:writer:cross-runtime: Node/stax-xml, Java/Woodstox, Rust/quick-xml의 실제 파일 writer 작업을 비교.pnpm --filter benchmark run release:reader:cross-runtime: Node/stax-xml, Java/Woodstox, Rust/quick-xml의 동일 pull-reader 작업을 비교.pnpm --filter benchmark run release:expanded: 전체 release benchmark set을 다시 생성합니다. Runtime matrix, 4 GiBStreamReaderSync, converter batch-plan, 1 GiB writer, parser fixture series, npm XML parser 비교, 1 MiB부터 4 GiB까지의 size series, writer small/big/async 행이 포함됩니다.
측정 기준
섹션 제목: “측정 기준”Parser benchmark는 라이브러리 기본값인 fragment mode의 공개 API로 JavaScript 문자열과 객체를 반환하는 경로를 측정합니다. raw byte만 훑는 작업은 완료된 parse로 보지 않습니다. 대용량 XML은 heap과 RSS를 별도로 기록하고, JavaScript 문자열 크기 제한을 피하면서 stream/event/converter API의 pull 방식 소비를 측정합니다.
릴리스 결과
섹션 제목: “릴리스 결과”생성된 환경과 runtime 버전은 아래에 표시됩니다. Runtime 행은 생성된 16 MiB fixture, warmup 1회, 측정 3회를 기준으로 합니다.
Benchmark 환경
섹션 제목: “Benchmark 환경”- Generated: 2026-07-18T17:16:41.036Z
- Run ID:
2026-07-18T17-16-41-036Z - CPU: Apple M4 (~2.40 GHz)
- 런타임: node 26.5.0 (arm64-darwin)
--expose-gc - 패키지 매니저: 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
- 소스: release aggregation
해결된 패키지와 런타임 버전
| 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
섹션 제목: “Parser Fixture Series”아래 행은 같은 fixture에서 동일한 최종 JavaScript object shape를 만드는 공개 StAX-XML surface들을 비교합니다. 별도의 npm parser 비교나 대용량 traversal 표와 섞어서 보지 말고 fixture 크기 순서대로 읽는 것이 기준입니다.
작은 문서 (2 KiB)
섹션 제목: “작은 문서 (2 KiB)”벤치마크 소스: parser-2kb.mjs, release aggregation
| Surface | 평균 시간 | 초당 작업 수 | Heap | 비고 |
|---|---|---|---|---|
| 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 |
중간 문서 (4 KiB)
섹션 제목: “중간 문서 (4 KiB)”벤치마크 소스: parser-4kb.mjs, release aggregation
| Surface | 평균 시간 | 초당 작업 수 | Heap | 비고 |
|---|---|---|---|---|
| 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 |
중대형 문서 (13 MiB)
섹션 제목: “중대형 문서 (13 MiB)”벤치마크 소스: parser-13mb.mjs, release aggregation
| Surface | 평균 시간 | 초당 작업 수 | Heap | 비고 |
|---|---|---|---|---|
| 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 |
큰 문서 (98 MiB)
섹션 제목: “큰 문서 (98 MiB)”벤치마크 소스: parser-98mb.mjs, release aggregation
| Surface | 평균 시간 | 초당 작업 수 | Heap | 비고 |
|---|---|---|---|---|
| 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
섹션 제목: “Maintained Node XML Parsers”이 표는 현재 benchmark workspace에 설치된 공개 npm XML parser package를 같은
books.xml fixture로 비교합니다. Object parser는 각자의 string-to-object API를
사용하고, event parser는 event name, text, attribute를 checksum으로 접어 parse
작업이 제거되지 않도록 합니다.
벤치마크 소스: release aggregation, books fixture
| 라이브러리 | 평균 시간 | 초당 작업 수 | Heap | 비고 |
|---|---|---|---|---|
| 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 |
다른 언어 Pull Reader 비교
섹션 제목: “다른 언어 Pull Reader 비교”각 측정은 동일한 UTF-8 파일을 읽고 parsing하는 전체 구간을 포함합니다. 각 공개 pull reader가 element 이름, 공백이 아닌 text, attribute 이름과 값을 모두 materialize하며, event 수와 checksum이 일치해야 결과를 기록합니다.
벤치마크 소스: cross-language reader
| Reader | Median throughput | Median time | 이벤트 수 | 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
섹션 제목: “StreamReaderSync Size Series”대용량 input series는 생성된 byte batch와 공개 StreamReaderSync
index API를 사용합니다. byte offset 기반 reader가 전체 XML을 하나의
JavaScript string으로 만들지 않고도 문자열 크기 제한을 넘어설 수 있다는
release evidence입니다.
벤치마크 소스: release aggregation, 4 GiB harness
| 크기 | 처리량 | 평균 | Heap delta | RSS delta | 이벤트 수 | 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 처리량 비대칭
섹션 제목: “Reader/Writer 처리량 비대칭”1 GiB reader 행과 writer 행을 나란히 보면 이상해 보일 수 있습니다. 두 행은 의도적으로 다른 작업을 측정합니다. 하드코딩된 처리량 수치 대신 생성된 표를 기준으로 비교하세요.
Writer는 대부분 deterministic append 작업입니다. 호출자는 element name,
attribute, text value를 이미 알고 있습니다. 그래서 WriterSyncSink는 자체
writer state를 검증하고, 이미 존재하는 JavaScript string을 encoding한 뒤,
큰 sequential chunk를 sink나 file descriptor에 flush합니다. 임의의 XML에서
delimiter를 찾거나, chunk boundary를 넘어 token을 복구하거나, incoming byte에서
name/text/attribute를 새로 발견하지 않습니다. 그래서 sync file writer는 다른
runtime의 native writer와 같은 넓은 처리량 구간에 들어갈 수 있습니다.
StreamReaderSync는 CPU-bound parsing입니다. 현재 1 GiB 행은 file I/O가 아니라
generated byte batch를 사용하므로 storage speed가 병목이 아닙니다. Reader는 모든
byte를 scan하고, markup과 text를 분류하고, XML state를 유지하고, event batch를
만들고, 안정적인 accessor API를 유지하면서, consumer가 요청할 때 name/text/
attribute를 JavaScript string으로 decode/materialize해야 합니다. 핵심 제한은
안정적인 StAX API 아래에서 수행되는 pure-JavaScript byte scanning과 UTF-8 span
decode/string materialization입니다. Woodstox나 quick-xml 같은 native parser는
delimiter search와 tokenization을 JVM/Rust 코드와 더 낮은 수준의 buffer access에
둘 수 있습니다.
Native addon이나 FFI-style 실험도 이 public contract 경계를 제거하지는 못했습니다. Rust, C, C++ tokenizer는 delimiter search 비용을 줄일 수 있고, 더 낮은 수준의 boundary는 pointer, buffer, span을 더 직접적으로 노출할 수 있습니다. 하지만 일반 JavaScript consumer에게 JavaScript string을 포함한 zero-copy StAX event stream을 완성된 형태로 건넬 수는 없습니다. Benchmark contract가 JavaScript string과 event를 요구하는 순간, V8 heap object를 만들거나 copy해야 하며, 이 materialization 비용이 tokenizer 구현 언어나 boundary 선택보다 더 지배적입니다.
이것이 순수 JavaScript release의 핵심 tradeoff입니다. Bounded memory와 portable byte-offset parsing을 우선하고, parser 처리량은 JavaScript tokenizer와 string materialization 비용의 제한을 받습니다.
Runtime Matrix
섹션 제목: “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 |
과거 4 GiB generated stream fixture는 Iterable<Uint8Array[]> 위의
StreamReaderSync를 fragment mode로 사용하고 current-token accessor로
소비합니다.
| 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 |
기타 벤치마크 항목
섹션 제목: “기타 벤치마크 항목”릴리스 matrix는 전체 benchmark suite의 일부입니다. Parser, stream reader, converter, builder, writer, async writer, large writer sink benchmark는 release-facing 측정 surface이며 선택적인 부록이 아닙니다.
| 항목 | 명령 | 측정 내용 |
|---|---|---|
| Parser fixture series | pnpm --dir packages/benchmark bench:sync |
2 KiB, 4 KiB, 13 MiB, 98 MiB parser fixture와 sync builder 결과. |
| Builder small/large | pnpm --dir packages/benchmark bench:builder:small, bench:builder:big |
Builder에 맞춘 중간 데이터에서 XML을 생성하는 비용. |
| Converter schema | pnpm --dir packages/benchmark bench:converter:compiled-batch |
Manual StreamReaderSync projection과 public schema.parseSync(bytes) 경로 비교. |
| Async parser/writer | pnpm --dir packages/benchmark bench:async |
Async parser와 async-vs-sync writer API overhead. |
| Writer core | pnpm --dir packages/benchmark bench:writer:core |
Compact/pretty output에 대한 direct writer method throughput. |
| 1 GiB writer | pnpm --dir packages/benchmark bench:writer:1gb:json |
Memory/temp-file target 위의 async writer와 sync sink writer. |
대용량 XML 출력에서 전체 XML 문자열을 보관하지 않고 조금씩 쓰려면
WriterSyncSink가 계속 권장되는 동기 경로입니다. 생성된 release data에는
1 GiB writer 결과도 포함됩니다.
Converter API 비교
섹션 제목: “Converter API 비교”Converter 비교는 같은 byte fixture를 다음 경로로 파싱합니다.
schema.parseSync(bytes): 지원되는 streaming dispatch plan을 자동 compile하고 cache합니다.
두 converter 행은 측정 전에 수동 기준값과 동일한 결과를 만드는지 검증합니다.
벤치마크 소스: converter-compiled-batch-plan.mjs, release aggregation
| 항목 | 처리량 | 평균 | 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 성능
섹션 제목: “Writer 성능”Writer benchmark는 builder-friendly input normalization을 timed region 밖에 둡니다. 따라서 행들은 반복적인 JSON shape 변환이 아니라 XML emission throughput을 측정합니다.
작은 문서 생성
섹션 제목: “작은 문서 생성”벤치마크 소스: writer release aggregation
| 라이브러리 | 평균 시간 | 초당 작업 수 | Heap | 비고 |
|---|---|---|---|---|
| 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 |
큰 문서 생성
섹션 제목: “큰 문서 생성”벤치마크 소스: writer release aggregation
| 라이브러리 | 평균 시간 | 초당 작업 수 | Heap | 비고 |
|---|---|---|---|---|
| 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 비교
섹션 제목: “Async vs Sync Writer 비교”벤치마크 소스: writer release aggregation
| Element Count | Async Writer | Sync Writer | Sync Sink | 성능 비율 |
|---|---|---|---|---|
| 1K elements | 4.26 ms | 1.98 ms | 2.18 ms | 1.95x 빠름 (sink) |
| 5K elements | 16.69 ms | 6.05 ms | 7.22 ms | 2.31x 빠름 (sink) |
| 10K elements | 30.13 ms | 14.84 ms | 12.33 ms | 2.44x 빠름 (sink) |
1 GiB Writer 비교
섹션 제목: “1 GiB Writer 비교”벤치마크 소스: writer-1gb.mjs, release aggregation
| 대상 | 평균 시간 | 처리량 | Heap | Peak RSS | 기록량 | 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 |
다른 언어 writer와의 비교
섹션 제목: “다른 언어 writer와의 비교”각 공개 writer API가 같은 compact XML을 생성해 실제 파일 sink에 기록합니다. Node 전용 1 GiB 측정과는 별도의 suite입니다.
동일한 generator로 12,798개 레코드를 compact XML로 만들고 실제 파일에 기록했습니다. 각 행은 3회 실행의 중앙값입니다.
| Writer | 중앙 처리량 |
|---|---|
| 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 |
입력 크기, warm-up, sink와 fixture가 다른 1 GiB Node 전용 측정과 직접 숫자를 섞어 비교하지 않습니다.
Current-Token Reader 소비 방식
섹션 제목: “Current-Token Reader 소비 방식”대용량 byte input에서는 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); } }}for (const event of batch)도 계속 지원하지만 wrapper event object를
할당합니다. 98 MiB 표준 fixture에서 index-first 소비는 730.56 ms,
heap delta 약 39.8 MiB였고, wrapper iteration은 1.28 s, heap delta 약
482.7 MiB였습니다.