Skip to content
Ferromark

Ferromark / Shared reference

Parser Benchmarks

V2 benchmark reports preserve source revisions, compiler flags, output checks, and raw measurements. Results describe the recorded workloads and machines.

Compare throughput

Choose a platform to compare native and Node.js libraries on the same recorded host. Each speedup includes all 57 documents, including inputs whose output differs. Follow a number for the machine, source revision and measurement date. Specification agreement is measured separately, with each library's public spec settings.

Ferromark speedup over each library · macOS arm64 · fresh calls
Compared withSpeedup
Native
Bun MD*Rust6.1×
cmarkC6.7×
cmark-gfmC7.1×
ComrakRust7.1×
Goldmark*Go9.8×
markdown-rsRust72.6×
md4c*C3.5×
OX-Content*Rust1.3×
pulldown-cmarkRust2.7×
Node.js
commonmark.jsJavaScript4.5×
markdown-it*JavaScript10.5×
marked*JavaScript9.5×
MD4X*Native addon1.5×
micromark*JavaScript97.9×
OX-Content*Native addon1.3×
remark / unified*JavaScript110.6×
Remarkable*JavaScript4.0×
Sätteri*Native addon1.8×
Showdown*JavaScript43.3×
TanStack Markdown*JavaScript4.8×
2× means twice the throughput. Node.js includes binding overhead. Every factor includes all 57 frozen inputs.
* Output differs for some of the 57 inputs on this platform. All inputs contribute to the performance factor. See the linked reports for syntax and API differences.

Latest complete comparison

The homepage uses these complete campaigns. All 57 frozen inputs contribute, with different outputs marked by an asterisk. Each platform ran on its own recorded host; each campaign contains three independent process rounds.

Linux x86-64: Blacksmith VM, 4 vCPU, 15.4 GiB observed memory (workflow). macOS arm64: Blacksmith VM, 6 vCPU, 24.0 GiB observed memory (workflow).

Manual comparison — Linux x86-64

All 20 selected comparisons were remeasured on one host from the same committed source revision. Factors mean Ferromark throughput relative to the library. Native and Node.js calls have separate build and allocation contracts.

RuntimeProjectTimed documentsFreshReuse
Nativepulldown-cmark57/572.35×2.43×
Nativemd4c*57/572.95×3.06×
NativeBun MD*57/574.74×5.15×
NativeOX-Content*57/571.40×1.37×
Nativemarkdown-rs57/57106.55×118.08×
NativeComrak57/575.10×5.51×
Nativecmark57/575.70×6.25×
Nativecmark-gfm57/576.15×6.67×
NativeGoldmark*57/578.77×9.49×
Node.jsmarked*57/579.74×14.46×
Node.jsmarkdown-it*57/5710.49×15.63×
Node.jsremark / unified*57/57132.49×192.30×
Node.jsmicromark*57/57108.26×165.69×
Node.jsShowdown*57/5747.32×69.00×
Node.jscommonmark.js57/574.76×7.02×
Node.jsRemarkable*57/573.88×5.77×
Node.jsSätteri*57/571.68×2.49×
Node.jsMD4X*57/571.33×1.96×
Node.jsOX-Content*57/571.27×1.86×
Node.jsTanStack Markdown*57/575.72×8.53×

Machine: AMD EPYC, 15.4 GB RAM, Linux 6.6.141. Measurement dates: 2026-10-01, source 9af95fc3.

*Output differs for some inputs. Every factor includes all 57 frozen documents; HTML agreement is retained as descriptive metadata, not a scoring filter or a conformance test. Public syntax, output, allocator, runtime and GC contracts remain documented; these figures compare the public APIs on the same inputs, not identical implemented features. Timing profile: balanced; 3 process rounds, 3 alternating 10 ms windows and 30 ms warmup. Equal-document geometric means use per-engine medians of round medians. Rotating-document cache controls remain separate. Native and Node binding costs remain distinct. Inspect round spread before making a performance claim; the balanced profile has less timing evidence than standard.

Builds and downloads finish before verification and timing. Lanes run sequentially with a cooldown; host observations and per-round ranges remain in the raw evidence. Background system activity can still add noise. These results describe this machine and corpus, with no significance claim. Other platforms and earlier reports remain separate.

Raw evidence, manual workflow.

Manual comparison — macOS arm64

All 20 selected comparisons were remeasured on one host from the same committed source revision. Factors mean Ferromark throughput relative to the library. Native and Node.js calls have separate build and allocation contracts.

RuntimeProjectTimed documentsFreshReuse
Nativepulldown-cmark57/572.72×2.77×
Nativemd4c*57/573.50×3.64×
NativeBun MD*57/576.05×6.50×
NativeOX-Content*57/571.34×1.30×
Nativemarkdown-rs57/5772.58×87.60×
NativeComrak57/577.14×8.45×
Nativecmark57/576.66×8.00×
Nativecmark-gfm57/577.10×8.42×
NativeGoldmark*57/579.75×11.73×
Node.jsmarked*57/579.49×14.03×
Node.jsmarkdown-it*57/5710.46×15.95×
Node.jsremark / unified*57/57110.57×172.85×
Node.jsmicromark*57/5797.87×146.97×
Node.jsShowdown*57/5743.32×65.32×
Node.jscommonmark.js57/574.51×6.87×
Node.jsRemarkable*57/573.96×5.93×
Node.jsSätteri*57/571.83×2.79×
Node.jsMD4X*57/571.51×2.24×
Node.jsOX-Content*57/571.35×2.03×
Node.jsTanStack Markdown*57/574.82×7.22×

Machine: Apple M4 Pro (Virtual), 24 GB RAM, macOS 26.3. Measurement dates: 2026-10-01, source 9af95fc3.

*Output differs for some inputs. Every factor includes all 57 frozen documents; HTML agreement is retained as descriptive metadata, not a scoring filter or a conformance test. Public syntax, output, allocator, runtime and GC contracts remain documented; these figures compare the public APIs on the same inputs, not identical implemented features. Timing profile: balanced; 3 process rounds, 3 alternating 10 ms windows and 30 ms warmup. Equal-document geometric means use per-engine medians of round medians. Rotating-document cache controls remain separate. Native and Node binding costs remain distinct. Inspect round spread before making a performance claim; the balanced profile has less timing evidence than standard.

Builds and downloads finish before verification and timing. Lanes run sequentially with a cooldown; host observations and per-round ranges remain in the raw evidence. Background system activity can still add noise. These results describe this machine and corpus, with no significance claim. Other platforms and earlier reports remain separate.

Raw evidence, manual workflow.

Native engine comparison

Historical comparison, measured 2026-09-24. These original agreement-subset scores remain separate from the latest complete campaigns.

Ferromark v2, the original OX-Content core, Ferromark v1, md4c, pulldown-cmark, and Bun's native bun_md engine parse and render the frozen 57 documents (37–113,609 UTF-8 bytes) with explicit syntax and renderer flags, in one native executable per platform. Each platform below has its own run, its own archived report, and its own figures: an Apple Silicon figure does not describe x86-64 or the reverse, because SIMD paths, compiler back ends, and cache hierarchies differ. Each scored column uses the same input set and equivalent HTML for every included engine. Speed relative to v2: higher is faster; v2 = 1.00×.

Only documents on which the engines produce equivalent HTML are scored. The all-six columns require agreement among all six engines; the broader five-engine columns require it among v2, v1, md4c, pulldown-cmark, and Bun. OX has no score in the five-engine columns: its original renderer cannot disable heading IDs, callouts, or fence metadata cleanup, and its heading-ID-only differences are not normalized away. Documents outside an agreement subset remain measured as diagnostics in each report.

All engines parse and render natively. The CommonMark lane disables optional syntax; the extension lane enables only tables, strikethrough, and task lists. It is not full GFM. Bare URL autolinking, footnotes, frontmatter, line comments, definition lists, MDX, and optional renderer extras are off where configurable. Raw HTML passes through; these explicit benchmark settings differ from library defaults.

Fresh includes parser/renderer setup, complete processing, owned output, and destruction. Reuse retains state where the public API permits; Bun's native bun_md still uses its fresh owned-output API. No JavaScript, WASM, process startup, file I/O, or output normalization is timed. On each platform all six engines share one executable, one pinned Rust compiler, mimalloc, and the same pinned registry resolution, and alternate in rotating windows within the same process rounds; the few build differences between the platforms are listed in the harness README. These are measured corpus aggregates on one machine per platform, not a universal ranking or a significance claim.

Ferromark's published binaries are built with profile-guided optimization. Where a report includes it, the same recipe was applied to every Rust engine in the shared executable and measured on held-out documents no profile saw. md4c is C and is not PGO-built; a PGO row is compared only with PGO rows.

Apple Silicon

Apple M1 Pro, measured 2026-09-24: v2 09e5e866 and v1 4e15141 in one macOS arm64 executable, 3 process rounds with 6 rotating windows per round.

EngineFresh, 14 agreeing across sixReuse, same 14Fresh, 50 agreeing across fiveReuse, same 50
Ferromark v21.00×1.00×1.00×1.00×
OX-Content original0.80×0.88×——
Ferromark v10.57×0.63×0.47×0.50×
md4c0.21×0.18×0.28×0.26×
pulldown-cmark0.37×0.34×0.37×0.36×
Bun native bun_md0.13×0.11×0.16×0.15×

On the 50 documents all five configurable engines render alike, v2 runs at 2.14× v1's speed fresh and 2.01× with reuse, 2.72× pulldown-cmark's, 3.62× md4c's, and 6.24× Bun's native engine's fresh. On the 14 documents all six engines agree on, it runs at 1.25× OX's throughput fresh and 1.14× with reuse.

Selected groups, fresh, using only the five-engine agreement subset and the same speed scale:

Group, five-engine agreementNV1md4cpulldownBun native
<512 B100.56×0.20×0.43×0.20×
32–128 KiB70.48×0.24×0.32×0.09×
comments110.58×0.22×0.43×0.19×
technical-docs210.46×0.32×0.36×0.16×
plain-prose40.56×0.23×0.26×0.05×

On the 28 held-out documents no profile saw, profile-guided optimization gives v2 1.23× fresh and 1.22× with reuse over its default build; against the PGO builds of the other engines, v2 stands at 2.18× v1, 2.66× pulldown-cmark, 4.34× md4c, and 5.89× Bun's native engine fresh.

Full results and per-document timings, held-out PGO comparison, and source and build provenance include raw measurements, source hashes, and reproducible configuration.

Linux x86-64

GitHub-hosted runner, AMD EPYC 7763, measured 2026-09-24: v2 09e5e866 and v1 4e15141 in one Linux x86-64 executable, 3 process rounds with 6 rotating windows per round.

The runner is a virtual machine on shared hardware. All six engines alternated in the same process rounds and rotating windows on that one host, so the ratios between engines are meaningful; absolute times are not comparable with any other run. See what a CI report can and cannot claim.

EngineFresh, 14 agreeing across sixReuse, same 14Fresh, 50 agreeing across fiveReuse, same 50
Ferromark v21.00×1.00×1.00×1.00×
OX-Content original0.72×0.73×——
Ferromark v10.59×0.62×0.48×0.50×
md4c0.24×0.20×0.31×0.29×
pulldown-cmark0.42×0.36×0.40×0.39×
Bun native bun_md0.20×0.16×0.21×0.20×

On the 50 documents all five configurable engines render alike, v2 runs at 2.09× v1's speed fresh and 2.00× with reuse, 2.48× pulldown-cmark's, 3.24× md4c's, and 4.68× Bun's native engine's fresh. On the 14 documents all six engines agree on, it runs at 1.39× OX's throughput fresh and 1.36× with reuse.

Selected groups, fresh, using only the five-engine agreement subset and the same speed scale:

Group, five-engine agreementNV1md4cpulldownBun native
<512 B100.58×0.25×0.51×0.29×
32–128 KiB70.48×0.23×0.30×0.12×
comments110.59×0.26×0.51×0.29×
technical-docs210.45×0.34×0.38×0.21×
plain-prose40.59×0.22×0.26×0.09×

On the 28 held-out documents no profile saw, profile-guided optimization gives v2 1.28× fresh and 1.26× with reuse over its default build; against the PGO builds of the other engines, v2 stands at 2.10× v1, 2.26× pulldown-cmark, 3.84× md4c, and 5.12× Bun's native engine fresh.

Full results and per-document timings, held-out PGO comparison, and source and build provenance include raw measurements, source hashes, and reproducible configuration.

Repeat runs on other runners show how much these figures move between runs and runner CPUs.

Earlier comparisons

Earlier native comparisons remain archived as historical evidence: 39b1f0b7 (2026-09-21, after optimization round 4), 60602a5a (2026-09-21, the repaired release head, before round 4), bffc89f6 (2026-09-21, the regressed release head), 7c887a2b (2026-09-16), c232d97 (2026-09-15), e93394ee (2026-09-15), and 33c216bf (2026-09-14). The full 207-case before/after suite uses a separate harness and reports all four stages.

The historical two-engine broad Markdown comparison measures 57 cases from 37 bytes to 114 KB: short comments, real documentation, and Wikipedia-derived prose. It separates input size, content, output agreement, and fresh/reused lifecycles. Full tables and raw data and the earlier synthetic diagnostic comparison use the same pinned parser binaries.

markdown-rs and micromark

Two separate pairs extend the frozen 57-document corpus comparison. These are new measurements of the recorded main revision, with no core changes. Their runtime and allocator settings differ from the archived six-engine comparison; the results are not pooled. Values below are Ferromark throughput relative to the competitor: higher is faster.

TrackCompetitorAgreeing documentsFreshReuse
Nativemarkdown-rs 1.0.057/5768.03×75.39×
Nodemicromark 4.0.256/5781.19×123.83×

The native pair uses rustc 1.95.0 (59807616e 2026-04-14), a shared system allocator, optimization level 3, fat LTO, one codegen unit, and a generic CPU target. The Node pair uses Node v24.21.0, Ferromark's local addon built with the release-node profile without PGO, and both public JavaScript string APIs. It includes wrapper/N-API conversion costs and JavaScript GC. The Node result describes application calls, not isolated language overhead.

CommonMark disables optional syntax. The extension lane enables only tables, strikethrough and task lists, with raw HTML passthrough. It is not full GFM; MDX, math, frontmatter, bare URL autolinking, footnotes, heading IDs and other renderer extras are off. The competitors expose no retained parser/output API: their reuse column retains only configuration and makes fresh HTML calls.

Every input is checked in both lifecycles before timing and again around timed windows. The score uses only exact or serialization-equivalent HTML pairs. Micromark differs on one Vite document's code-block line breaks; that document remains a measured diagnostic and is excluded from the score. Code whitespace is not normalized away. Three process rounds with six rotating engine-order windows per round use equal-document geometric means. These are measurements on a shared macOS arm64 workstation, not a universal ranking or a significance claim. MDX and AST capabilities belong in the separate feature inventory.

Raw results, source hashes, and output differences, reproduction commands.

Node.js ecosystem coverage

Five further public Markdown-to-HTML APIs use the same frozen 57 inputs. Each is independently paired with Ferromark's local Node addon without PGO. The core and addon source hashes match the earlier micromark run; the adapter commit is recorded separately. Ratios use only each pair's agreeing documents, so these rows do not establish a shared-set ranking.

ProjectVersionAgreeing documentsFreshReuse
marked18.0.1456/578.66×13.26×
markdown-it15.0.256/579.39×14.21×
remark / unified15.0.156/5792.18×139.01×
Showdown2.1.031/5737.57×66.44×
commonmark.js0.31.257/574.08×6.28×

commonmark.js runs CommonMark only in both engines on all inputs, because its public parser has no GFM extension lane. All other pairs use the frozen per-document CommonMark or tables/strikethrough/task-list profile. marked uses its public URL-tokenizer override to disable literal autolinking. markdown-it keeps its native <s> spelling and task-list plugin classes; remark includes remark-rehype and rehype-stringify, with only the three matched syntax extensions. Showdown keeps its own Markdown dialect, extra fenced-code classes, and task-list styles. Its smaller agreement set also includes differences in custom HTML and Markdown parsing; the retained outputs show each excluded document. Heading IDs, metadata, ellipsis conversion, and optional renderer extras are disabled.

All pairs retain configured public parser/processor objects outside timing; each call parses and returns a new JavaScript HTML string. Both Ferromark lifecycles are verified before timing and around every process's windows. Three process rounds, six alternating windows, 40 ms timing windows, and 60 ms warmup use the same median/geometric-mean aggregation as the micromark pair. The strict output classifier is unchanged; differing text, tags, task classes, styles, code whitespace, and other attributes are not discarded to improve agreement. Disagreements remain timed diagnostics and are excluded from ratios.

These macOS arm64 workstation results describe these inputs and build profiles; Linux cells stay unmeasured. Configuration, agreement counts, raw windows, locks, source hashes, and adapter snapshots are retained in the report below. Reproduction and adapter contracts, project coverage and framework integrations.

Completed native and Linux comparisons

These independently measured pairs fill the remaining homepage cells. The Ferromark core source hashes match the earlier clean-core ecosystem runs. Factors mean Ferromark throughput relative to the library. Each row uses its own equivalent-HTML set; rows do not establish a shared-set ranking.

PlatformProjectEquivalent documentsFreshReuse
macOS arm64Comrak57/577.83×8.60×
macOS arm64cmark57/577.01×7.84×
macOS arm64cmark-gfm57/577.08×7.85×
Linux x86-64Comrak57/575.05×5.55×
Linux x86-64cmark57/575.60×6.18×
Linux x86-64cmark-gfm57/575.82×6.38×
Linux x86-64markdown-rs57/5799.64×109.52×
Linux x86-64micromark56/57105.10×152.72×
Linux x86-64marked56/5710.45×15.25×
Linux x86-64markdown-it56/5711.11×15.87×
Linux x86-64remark / unified56/57123.65×176.62×
Linux x86-64Showdown31/5734.11×55.95×
Linux x86-64commonmark.js57/574.95×7.28×

cmark 0.31.2 uses CommonMark only in both engines for every document, as commonmark.js does. Comrak 0.55.0 and cmark-gfm 0.29.0.gfm.13 enable only tables, strikethrough, and task lists on the extension inputs. All use their public Markdown-to-HTML APIs and preserve raw HTML. The C adapters call native library functions directly; no CLI startup or IPC is timed. cmark's convenience call and cmark-gfm's parser/feed/finish/render sequence own fresh parser/AST/output state in both schedules. C HTML consumption includes strlen before freeing the returned allocation. Comrak uses markdown_to_html, also with fresh owned AST/output. Reuse retains configuration in these competitors; Ferromark retains its arena and renderer. AST and output destruction remain within native timing.

The native pairs use system malloc, Rust 1.95.0, generic CPU, optimization level 3, fat LTO, one codegen unit, and panic abort. C libraries/adapters use clang -O3, system malloc, and no PGO; no allocator redirection or cross-language LTO. The Node pairs use Node 24.21.0 and the release-node addon without PGO, including public binding/string conversion costs and JavaScript GC.

Every pair has three process rounds, six rotating 40 ms windows and 60 ms warmup, with verification before timing and around each round. All 57 inputs, exact outputs, option guards, 2,052 paired windows, locks, source hashes, and committed adapter snapshots are retained. Only exact or conservative serialization- equivalent HTML contributes to factors; code whitespace, attributes, task-list markup, and text differences remain significant.

macOS runs use a shared Apple M1 Ultra workstation. Linux pairs each run on their own GitHub-hosted Ubuntu 24.04 x86-64 VM; the pair alternates on one VM, but the CPU/host can differ between pairs. Metadata records CPU, load, clocks, thermal observations and hypervisor steal counters. No other workflow step runs during timing. These are separate steady-state measurements, with no significance claim or comparison of absolute durations between hosts.

Raw evidence and per-pair host metadata, reproduction.