Skip to content

Performance#

View Markdown

This page tracks benchmark results, bundle-size checks, and the optimizations that affect shipped output. Architecture and package pages should describe boundaries and APIs; measured performance belongs here.

For allocation and span-level investigation while developing parser or renderer changes, use Profiling Mode. Profiling answers "where is the work happening?" Benchmarking answers "how fast is this workload?"

What We Measure#

Ox Content has two performance surfaces:

  • Runtime throughput for Markdown parsing and rendering.
  • Static output weight for generated sites, including gzip size and initial request count.

Runtime matters for CLIs, dev servers, editor integrations, and batch builds. Output weight matters for documentation sites because generated HTML, CSS, and JS are what users fetch on every navigation.

Runtime Snapshot#

Ox Content is positioned both as a document generator and as a high-performance Markdown toolkit. The numbers below focus on the Markdown engine side.

Speed is only half of a fair comparison: Markdown engines differ in how much of CommonMark they implement, and some deliberately trade spec coverage for throughput. Every row therefore carries a measured CommonMark conformance rate next to its speed, so a faster engine that skips spec behavior is visible as such rather than simply ranking higher. See CommonMark Conformance for how Ox Content scores and how the rate is measured.

Benchmark sweep generated on 2026-08-17 (median of 7 runs). Numbers track the host machine; the relative ordering between engines is the stable signal. Regenerated by scripts/render-benchmark-tables.mjs.

Environment: runner blacksmith-32vcpu-ubuntu-2404, Node v24.19.0, Bun 1.3.14, CPU Intel(R) Xeon(R) Processor, 32 logical cores.

CommonMark column: share of the 652 CommonMark 0.31.2 spec examples an engine renders correctly, measured by benchmarks/commonmark-conformance/run.mjs. Each engine runs in the most spec-faithful configuration it exposes, and both sides of the comparison pass through the conformance suite's HTML normalizer, so engines are ranked by behavior rather than by markup spelling.

Parse Only (48.7 KB)#

Library ops/sec avg time throughput CommonMark
ox-content (native) 7,739 0.13 ms 368.27 MB/s 100.0%
pulldown-cmark 4,941 0.20 ms 235.14 MB/s 100.0%
xai-grok-markdown-core (Grok Build) 4,233 0.24 ms 201.44 MB/s 100.0%
@ox-content/napi 3,479 0.29 ms 165.57 MB/s 99.5%
satteri 1,521 0.66 ms 72.37 MB/s 98.9%
md4x (napi) 1,157 0.86 ms 55.08 MB/s 99.5%
md4x (wasm) 967 1.03 ms 46.02 MB/s 99.5%
md4w (md4c) 931 1.07 ms 44.30 MB/s 91.7%
markdown-it-ts 876 1.14 ms 41.68 MB/s 100.0%
@tanstack/markdown 684 1.46 ms 32.53 MB/s 47.4%
marked 420 2.38 ms 19.96 MB/s 93.4%
markdown-it 285 3.51 ms 13.56 MB/s 100.0%
@mizchi/markdown 59 16.81 ms 2.83 MB/s 45.9%
remark 33 30.28 ms 1.57 MB/s 99.8%

Parse + Render (48.7 KB)#

Library ops/sec avg time throughput CommonMark
ox-content (native) 5,879 0.17 ms 279.78 MB/s 100.0%
@ox-content/napi 5,424 0.18 ms 258.13 MB/s 99.5%
pulldown-cmark + push_html 4,733 0.21 ms 225.24 MB/s 100.0%
md4x (napi) 3,122 0.32 ms 148.56 MB/s 99.5%
Bun.markdown.html 2,394 0.42 ms 113.93 MB/s 100.0%
md4x (wasm) 2,089 0.48 ms 99.43 MB/s 99.5%
md4w (md4c) 1,856 0.54 ms 88.31 MB/s 91.7%
satteri 1,200 0.83 ms 57.10 MB/s 98.9%
markdown-it-ts 767 1.30 ms 36.48 MB/s 100.0%
@mizchi/markdown 660 1.51 ms 31.42 MB/s 45.9%
@tanstack/markdown 450 2.22 ms 21.43 MB/s 47.4%
marked 381 2.62 ms 18.13 MB/s 93.4%
markdown-it 259 3.86 ms 12.34 MB/s 100.0%
micromark 35 28.83 ms 1.65 MB/s 100.0%
remark 28 35.95 ms 1.32 MB/s 99.8%

Parse Only (~1 MB)#

Library ops/sec avg time throughput CommonMark
ox-content (native) 362 2.76 ms 370.17 MB/s 100.0%
pulldown-cmark 234 4.28 ms 239.19 MB/s 100.0%
xai-grok-markdown-core (Grok Build) 201 4.98 ms 205.39 MB/s 100.0%
@ox-content/napi 199 5.03 ms 203.45 MB/s 99.5%
satteri 69 14.54 ms 70.36 MB/s 98.9%
md4w (md4c) 44 22.61 ms 45.26 MB/s 91.7%
md4x (napi) 42 24.05 ms 42.54 MB/s 99.5%
md4x (wasm) 39 25.73 ms 39.76 MB/s 99.5%
@tanstack/markdown 26 39.15 ms 26.13 MB/s 47.4%
markdown-it-ts 23 43.74 ms 23.39 MB/s 100.0%
marked 17 60.21 ms 16.99 MB/s 93.4%
markdown-it 10 95.62 ms 10.70 MB/s 100.0%
@mizchi/markdown 2 561.04 ms 1.82 MB/s 45.9%
remark 1 1533.48 ms 0.67 MB/s 99.8%

Parse + Render (~1 MB)#

Library ops/sec avg time throughput CommonMark
ox-content (native) 261 3.83 ms 267.26 MB/s 100.0%
@ox-content/napi 216 4.62 ms 221.33 MB/s 99.5%
pulldown-cmark + push_html 216 4.63 ms 221.16 MB/s 100.0%
md4x (napi) 133 7.51 ms 136.22 MB/s 99.5%
Bun.markdown.html 115 8.72 ms 117.27 MB/s 100.0%
md4w (md4c) 109 9.17 ms 111.55 MB/s 91.7%
md4x (wasm) 92 10.91 ms 93.79 MB/s 99.5%
satteri 64 15.56 ms 65.77 MB/s 98.9%
@mizchi/markdown 23 43.92 ms 23.30 MB/s 45.9%
markdown-it-ts 19 52.18 ms 19.61 MB/s 100.0%
@tanstack/markdown 18 55.33 ms 18.49 MB/s 47.4%
marked 15 68.43 ms 14.95 MB/s 93.4%
markdown-it 10 103.65 ms 9.87 MB/s 100.0%
micromark 1 746.23 ms 1.37 MB/s 100.0%
remark 1 1712.02 ms 0.60 MB/s 99.8%

The tables above are regenerated from a clean Blacksmith 32 vCPU CI environment by the Benchmark docs workflow; run OX_CONTENT_BENCHMARK_RUNS=7 vp run bench:docs to refresh them locally. Absolute ops/sec track the host (earlier sweeps on faster hardware report higher numbers); the relative ordering between engines is the stable signal.

On the 48.7 KB document the boundary-free native row leads the fastest other engine (pulldown-cmark) by ~1.6x on parse-only and ~1.2x on parse+render; at ~1 MB those leads are ~1.5x and ~1.2x. On parse+render the second row is now @ox-content/napi rather than a third-party engine, so the N-API boundary — not the engine — is what separates the top two rows there. The JavaScript-facing @ox-content/napi row is 4.0–5.1x faster than the two TypeScript renderers on parse-only and 7.1–12.1x faster on parse+render. At ~1 MB those N-API leads grow to 7.7–8.7x and 11.4–12.0x respectively, while the native pipeline sustains 267–370 MB/s. At that size the incremental CST parser (@mizchi/markdown, tuned for real-time editing rather than bulk parsing) falls to ~2 ops/sec on parse-only while recovering to ~23 ops/sec on parse+render, whereas the unified/remark pipeline stays at ~1 op/sec in both tables and micromark measures ~1 op/sec on parse+render.

The runtime sweep covers more than the tables above. The harness also runs small and medium Markdown inputs, an async parse+render target for the N-API package (so PR checks can catch JavaScript-boundary overhead regressions), and an optional Bun.markdown comparison when the harness is run under Bun.

The TypeScript renderer comparisons use each package's public default API. @tanstack/markdown is measured through parseMarkdown for parse-only and renderHtml for parse+render. markdown-it-ts reuses one initialized instance and measures parse and render, matching the existing markdown-it setup. Parser construction, module loading, and benchmark warmup happen outside the timed loop; each parse+render operation starts from the same Markdown string.

CommonMark Conformance#

Ox Content targets full CommonMark conformance. The engine is checked against the vendored CommonMark 0.31.2 specification on every CI run, not only when the benchmark tables are refreshed:

  • Core profile: 652 / 652 examples. cargo test -p ox_content_renderer --test spec_commonmark fails both when a passing example regresses and when a recorded failure starts passing, so the baseline cannot quietly drift.
  • GFM profile: 649 / 652 examples. The three differences are spec examples 608, 611, and 612, where the GFM autolink extension deliberately linkifies bare URLs and emails that plain CommonMark leaves as text. They are listed in crates/ox_content_renderer/tests/spec_fixtures/commonmark-known-failures.txt.
  • GFM extensions: every example in the GitHub Flavored Markdown 0.29-gfm spec sections for tables, task lists, strikethrough, autolinks, and disallowed raw HTML, driven by spec_gfm.rs.

Extensions beyond CommonMark — GFM tables, task lists, strikethrough, footnotes, and the built-in embeds — are opt-out rather than opt-in, so a document that uses none of them conforms to the specification under the normalization rule described below (ox-content adds slug id attributes to headings, so the HTML is not byte-identical to the spec's). Markdown Baseline lists each toggle.

Reading the CommonMark Column#

The rate beside each engine is measured, not claimed. benchmarks/commonmark-conformance/run.mjs renders all 652 spec examples with every engine in the tables and compares the output to the specification.

Two choices make that comparison fair, and both matter when reading the numbers:

  • Each engine runs in the most spec-faithful configuration it exposes, not in its benchmark defaults. markdown-it and markdown-it-ts use their commonmark presets, micromark and remark-html are told to pass raw HTML through instead of escaping it, and md4w runs with parseFlags: 0. Judging an engine by a default preset that enables GFM extensions would measure the preset rather than the engine. Engines that expose no such mode — marked, md4x, @tanstack/markdown, @mizchi/markdown — are scored as they ship.
  • Both sides pass through the conformance suite's HTML normalizer, so differences that do not change how a document renders (entity spelling, attribute order, <br /> vs <br>, whitespace between block tags) are not counted as conformance gaps. Without it the table would rank engines by markup spelling: ox-content scores 82.5% under byte-exact comparison purely because it adds slug id attributes to headings.

The comparison is symmetric — pulldown-cmark, an independent Rust implementation unrelated to ox-content, also scores 100% — so the normalizer is not tuned to any one engine.

Two rows are the same engine at different layers. ox-content (native) is the core profile at 100%; @ox-content/napi scores 99.5% because its defaults enable the bare-URL autolinking builtin, which linkifies examples 602, 608, and 611. Pass autolinkUrls: false to turn it off.

Regenerate the results with:

node benchmarks/commonmark-conformance/run.mjs --json benchmarks/commonmark-conformance/results.json

CJK Emphasis#

CommonMark's emphasis rules do not recognize ** placed immediately inside CJK punctuation, so A**強調。**B renders as literal text rather than bold. This affects every spec-conformant engine — it is a property of the specification's left/right-flanking delimiter rules, not an implementation gap. Emphasis that merely sits next to CJK charactersこれは**重要**です。 — is allowed by the specification and works everywhere.

Ox Content ships an opt-in deviation for it. cjkEmphasis classifies East Asian punctuation as an ordinary character when deciding whether a delimiter run may open or close, which lets those runs pair; halfwidth ASCII punctuation is left alone, so Latin documents parse identically. It is off by default, which is what keeps the 652/652 figure above true of the shipped default. See CJK Emphasis.

Bundle Size#

The bundle-size benchmark builds representative docs applications and measures the generated production output. It reports:

  • Total bytes across the generated output directory.
  • Gzipped bytes for JS, CSS, HTML, and JSON assets.
  • File count in the output directory.
  • Estimated initial requests for index.html and local assets referenced from HTML or CSS.

Latest local bundle-size sweep on 2026-05-28 with Node v24.16.0 on Apple M5 Pro. The table lists successful production builds from the local sweep.

App Total Gzipped Ratio Requests Files
ox-content (bare) 20.6 KB 5.8 KB 1.00x 1 5
ox-content 111.1 KB 25.6 KB 4.41x 4 9
VitePress (bare) 155.0 KB 46.9 KB 8.05x 6 14
ox-content + Vue 169.6 KB 47.9 KB 8.23x 4 9
VitePress 972.4 KB 717.2 KB 123.18x 21 29

ox-content (bare) is the no-JS baseline. ox-content includes the built-in docs shell. ox-content + Vue adds framework island support. VitePress rows use the same benchmark content so the comparison is focused on generated output shape rather than authoring content.

Bundle-size comparisons are intentionally reported beside request count. A smaller gzip number is not always better if it creates too many blocking requests; a larger shared chunk can be preferable when it removes repeated bytes from every generated page and is cacheable across navigation.

Chunk Optimization#

The SSG pipeline first renders complete HTML pages, then extracts shared assets after all pages are known. The TypeScript plugin calls the Rust-backed externalizeSsgAssets implementation, which rewrites each generated page and writes hashed assets under assets/.

The optimizer keeps the rules conservative:

  • Identical CSS and JS content is deduplicated by content, then emitted once with a content hash in the filename.
  • Core CSS sections such as base and footer styles are linked as a shared ox-content-core-*.css file.
  • Theme CSS stays inline when it is small or when it contains relative url(...) references, so path resolution does not change.
  • Search payload code is split from the main boot script into a separate ox-content-search-*.js chunk when the generated script contains the search placeholder.
  • Generated scripts are emitted with defer, and public asset paths honor the configured site base.

The goal is not to split every feature into a separate file. The docs shell is small enough that excessive chunking can increase startup work. The current policy favors stable shared chunks for repeated bytes and avoids moving content when doing so would create path-resolution risk or extra request overhead.

PR Regression Gate#

Pull requests run the benchmark workflow against both the base commit and the head commit on blacksmith-32vcpu-ubuntu-2404. The workflow posts one report with runtime, competitive snapshot, environment, and bundle-size sections.

Runtime rows compare the large benchmark target for @ox-content/napi and @ox-content/napi (async). Changes within +/-5% are treated as noise, and the check fails when head throughput is more than 10% slower than base.

The competitive snapshot is not a gate. It uses the head commit to show the target package against the next fastest comparison package for the same large input corpus.

Bundle rows compare gzipped output for each successful benchmark app. The check fails when gzipped size grows by more than 5%. Maintainers can intentionally accept a regression with the benchmark-regression-accepted PR label.

Reproduce#

Run the JavaScript benchmark harness from the repository root:

node benchmarks/bundle-size/parse-benchmark.mjs

The benchmark includes @tanstack/markdown, markdown-it-ts, md4w (md4c), and md4x (napi) by default, and adds Bun.markdown.html automatically when bun is available.

Run the bundle-size benchmark from the repository root:

node benchmarks/bundle-size/measure.mjs

For a faster local rerun after dependencies are installed, use:

node benchmarks/bundle-size/measure.mjs --skip-install

For Rust-side parser benchmarks, use:

cargo bench -p ox_content_parser

For real-world Markdown corpus benchmarks, populate the optional corpus first:

node scripts/fetch-bench-corpus.mjs
cargo bench -p ox_content_parser --bench corpus

For N-API transfer-format micro-benchmarks, see @ox-content/napi.

Last updated: