Performance#
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_commonmarkfails 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-itandmarkdown-it-tsuse theircommonmarkpresets,micromarkandremark-htmlare told to pass raw HTML through instead of escaping it, andmd4wruns withparseFlags: 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 slugidattributes 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.htmland 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-*.cssfile. - 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-*.jschunk when the generated script contains the search placeholder. - Generated scripts are emitted with
defer, and public asset paths honor the configured sitebase.
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.