High-performance JSDoc parser inspired by the oxc project.
Try the ox-jsdoc playground to inspect parsed JSDoc output in the browser.
- Parse JSDoc at native Rust speed when generating JSDoc documentation in
ox-content, a potential alternative to TypeDoc. - Speed up lint performance for
eslint-plugin-jsdocwhen it runs on Oxlint.
Benchmarks were run on 2026-05-15 against the post-cutover canonical Binary AST package (ox-jsdoc / @ox-jsdoc/wasm) and the typed AST reference (ox-jsdoc-origin / @ox-jsdoc/wasm-origin). All Rust-direct rows were re-measured in the same run on the same machine. Treat these numbers as a snapshot for this workspace, not as a stable performance guarantee.
See design/009-jsdoc-linter-benchmark/README.md for the benchmark design, fixture selection, measurement methodology, and result interpretation notes.
ox-jsdocis most useful when parsing can be batched. In the full-file parser-only benchmark,ox-jsdocNAPIparseBatchparses 226 comments in 300.720 us (1.331 us/comment), whilecomment-parserloop is 1.86x slower andjsdoccommentloop is 2.82x slower. The main win is avoiding per-comment JavaScript/native calls and repeated parser setup.- For ESLint-only usage, replacing the parser does not produce a large end-to-end lint speedup by itself. These runs are still dominated by ESLint, config loading, AST traversal, and, for TypeScript,
@typescript-eslint/parser. In this fixture set,eslint-ox-jsdoc-batchis essentially the same as upstream ESLint on JS (1.03x faster) and TS (1.01x faster). - The larger linting benefit appears when the same JSDoc rules run on Oxlint.
oxlint-ox-jsdoc-batchis 1.22x faster on JS and 5.19x faster on TS than upstream ESLint, even with the JS plugin bridge. Oxlint's built-in native JSDoc path is the fastest linter reference in this benchmark (3.11xon JS,93.76xon TS). - The Rust-direct table shows that the canonical batch path (
parse_batch_to_bytesat 885 ns/comment,parse_batch_intoat 978 ns/comment) is in the same neighbourhood asoxc_jsdoc's lazy parse (952 ns/comment), even thoughox-jsdoceagerly emits a full Binary AST whileoxc_jsdoconly walks tags lazily. Per-comment usage (parse_into,parse) is 1.25x–1.39x slower thanoxc_jsdocbecause the writer setup cost is paid for every comment instead of being amortized across a batch. - For
ox-contentdocumentation generation, the parser-only numbers are the more relevant signal than the ESLint-only runs: a Rust-native batch parser can keep JSDoc parsing a small part of the total documentation pipeline when many comments are parsed from a file at once. - The Rust-direct Criterion table is a Rust-side reference, not an end-to-end JavaScript benchmark. Use it to separate parser-core lower bounds, typed-AST vs Binary-AST emission cost, batch vs per-comment writer-setup cost, and bytes-output cost; use the Node / NAPI table for the practical cost seen from JavaScript.
| Item | Value |
|---|---|
| Machine | MacBook Pro |
| CPU | Apple M1 Max, 10 cores (8 performance + 2 efficiency) |
| Memory | 64 GB |
| Architecture | arm64 |
| OS | macOS 15.5 (24F74), Darwin 24.5.0 |
| Node.js | v24.15.0 |
| pnpm | 10.33.0 |
| Rust / Cargo | 1.94.1 |
| Hyperfine | 1.19.0 |
| Mitata | 1.0.34 |
- Linter benchmarks were run through
pnpm --filter @ox-jsdoc/benchmark benchmark:jsdoc-linter. - The linter script generates configs, then invokes
hyperfinedirectly from a shell script with--warmup 1,--runs 10, and--ignore-failure. - Each linter command is executed from inside the fixture directory so
.is resolved against the intended project tree. - Parser-only Node.js benchmarks were run through
pnpm --filter @ox-jsdoc/benchmark benchmark:parse-batch, which usesnode --expose-gc. - Parser-only Node.js timings use
mitata.measure()viatasks/benchmark/scripts/lib/measure.mjs: 10 rounds, discard the first round, trim the fastest and slowest remaining rounds, then report the mean of the remaining 7 round p50 values. - Rust-direct parser references were run with
cargo bench/ Criterion and are reported separately from Node / NAPI / WASM timings. The runs cover the canonicalox_jsdoc(Binary AST) entry points (parse/parse_into/parse_batch/parse_batch_into/parse_to_bytes/parse_batch_to_bytes) plusparse_block_into_data(phase 1 only), the typed AST entry pointox_jsdoc_origin::parse_comment, andoxc_jsdocas the Oxlint-native reference.
End-to-end CLI benchmark using Hyperfine. The combined rule set enables:
jsdoc/empty-tagsjsdoc/require-param-descriptionjsdoc/require-param-type
Fixtures:
- JS:
refers/eslint-plugin-jsdoc/src/- 86 files, 28,741 lines, 1,170 JSDoc blocks - TS:
refers/vscode/src/- 5,996 files, 1,951,540 lines, 25,238 JSDoc blocks
This README summarizes mean timings. Full median, p95, and standard deviation values are in tasks/benchmark/results/jsdoc-linter-hyperfine.md.
| Pattern | JS mean | JS speed vs baseline | TS mean | TS speed vs baseline |
|---|---|---|---|---|
eslint-jsdoc-upstream |
721.6 ms | 1.00x (baseline) | 40.316 s | 1.00x (baseline) |
oxlint-jsdoc-native |
231.8 ms | 3.11x faster | 430.0 ms | 93.76x faster |
eslint-ox-jsdoc-single |
797.6 ms | 1.11x slower | 41.018 s | 1.02x slower |
eslint-ox-jsdoc-batch |
701.3 ms | 1.03x faster | 39.994 s | 1.01x faster |
oxlint-ox-jsdoc-batch |
592.6 ms | 1.22x faster | 7.766 s | 5.19x faster |
In-process Node.js benchmark using the 226 JSDoc comments from fixtures/perf/source/typescript-checker.ts. These rows compare parser entry points and keep linter integration out of the timed path.
In the parser-only tables, loop means the benchmark calls the parser once per JSDoc comment in a JavaScript or Rust loop. It is the non-batch baseline used to show how much overhead parseBatch removes by parsing all comments in one call.
| Parser | Full file total | Per comment | vs canonical NAPI parseBatch |
|---|---|---|---|
ox-jsdoc NAPI (parseBatch) |
300.720 us | 1.331 us | 1.00x |
ox-jsdoc WASM (parseBatch) |
462.214 us | 2.045 us | 1.54x slower |
comment-parser (loop) |
559.584 us | 2.476 us | 1.86x slower |
jsdoccomment (loop) |
847.494 us | 3.750 us | 2.82x slower |
@ox-jsdoc/jsdoccomment (parseCommentBatch) |
1.142 ms | 5.055 us | 3.80x slower |
ox-jsdoc-origin NAPI (typed AST, loop) |
1.684 ms | 7.452 us | 5.60x slower |
@ox-jsdoc/jsdoccomment (parseComment loop) |
4.613 ms | 20.412 us | 15.34x slower |
Rust-direct parser references are separate Criterion measurements. They do not include Node.js, NAPI / WASM boundary cost, or @ox-jsdoc/jsdoccomment normalization. oxc_jsdoc is included as a Rust reference because it is the public API used by Oxlint's native JSDoc support.
The last column compares each row against oxc_jsdoc using faster / slower wording. Rows with different scenarios are not interchangeable feature-for-feature comparisons.
parse_block_into_datais the canonical crate's internal phase-1 parser. It walks one JSDoc block and builds intermediateBlockData/TagDataplus diagnostics, but does not emit Binary AST bytes. The benchmark loops over all 226 comments, so this is a parser-core lower-bound measurement.ox_jsdoc_origin::parse_commentis the typed AST entry point — it parses one block into an arena-allocated typed AST and skips Binary AST emission entirely. The gap between this and the canonicalparse_intois the cost of binary serialization.parse_batch_intoandparse_batchare the public batch APIs returning a typedBatchResult<'arena>.parse_batch_intoreuses a caller-suppliedBinaryWriterso per-call writer setup (StringTableBuilderprelude memcpy + arena buffer init) is amortized over the whole batch.parse_batchconstructs a fresh writer per call.parse_batch_to_bytesis the public bytes API used by the JS bindings. It accepts all comments as one batch, emits each parsed block throughBinaryWriter, and returns one owned Binary AST byte buffer with shared string data. This is the full Rust-side pipeline behindparseBatch, before Node / NAPI / WASM overhead.parse_intoandparseare the public per-comment APIs returningParseResult<'arena>.parse_intoreuses a caller-suppliedBinaryWriterso the writer setup cost is amortized across the loop.parseconstructs a fresh writer per call.parse_to_bytesis the per-comment bytes-only sibling ofparse_into/parse; it allocates a fresh writer and returns the ownedVec<u8>.
| Parser | Scenario | Estimate | Per comment | vs oxc_jsdoc |
|---|---|---|---|---|
ox_jsdoc |
parse_block_into_data, phase 1 block parse only, loop |
78.081 us | 345 ns | 2.76x faster |
ox_jsdoc_origin |
typed AST parse_comment, loop, shared arena |
121.21 us | 536 ns | 1.78x faster |
oxc_jsdoc |
JSDoc::new(inner, span).tags(), loop lazy parse |
215.21 us | 952 ns | 1.00x (baseline) |
ox_jsdoc |
parse_batch_to_bytes, single batch full pipeline |
199.93 us | 885 ns | 1.08x faster |
ox_jsdoc |
parse_batch, single batch, shared arena |
221.10 us | 978 ns | 1.03x slower |
ox_jsdoc |
parse_batch_into, single batch, shared writer |
221.11 us | 978 ns | 1.03x slower |
ox_jsdoc |
parse_into, public single-comment API, loop, shared writer |
269.65 us | 1.193 us | 1.25x slower |
ox_jsdoc |
parse, public single-comment API, loop, shared arena |
298.61 us | 1.321 us | 1.39x slower |
ox_jsdoc |
parse_to_bytes, public single-comment bytes API, loop |
303.54 us | 1.343 us | 1.41x slower |
This repository uses Vite+ as the task runner. Install vp before running the project tasks:
curl -fsSL https://vite.plus | bashCommon commands:
vpr build # or `vp run build`, build for JavaScript codes
vpr fmt # or `vp run fmt`, format for Rust and JavaScript codes
vpr check # or `vp run check`, lint for Rust and JavaScript codes
vpr test # or `vp run test`, test for Rust and JavaScript codesvpr check runs the Rust license-header task and cargo check. The header task checks Rust sources for:
- non-empty
@author @license MIT
The first run builds the local xtask crate automatically through Cargo. You can also run the task directly:
cargo run -p xtask -- headers:checkRust commands can be run directly as well:
cargo fmt --check
cargo check
cargo testThe development of ox-jsdoc is supported by my OSS sponsors!