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.
Latest local runtime sweep on 2026-05-30 with Node v24.16.0 on an Apple M2
Max. The tables show median results from 7 local runs of the benchmark harness,
covering the 48.7 KB "large" case and a ~1 MB "huge" case. Absolute ops/sec
track the host (earlier sweeps on faster hardware report higher numbers), but
the relative ordering between engines is stable.
Parse Only (48.7 KB)
| Library | ops/sec | avg time | throughput |
|---|---|---|---|
@ox-content/napi |
3272 | 0.31 ms | 155.72 MB/s |
satteri |
1682 | 0.59 ms | 80.06 MB/s |
md4w (md4c) |
670 | 1.49 ms | 31.89 MB/s |
md4x (napi) |
623 | 1.60 ms | 29.66 MB/s |
markdown-it |
596 | 1.68 ms | 28.37 MB/s |
marked |
386 | 2.59 ms | 18.38 MB/s |
@mizchi/markdown |
41 | 24.19 ms | 1.97 MB/s |
remark |
27 | 36.56 ms | 1.30 MB/s |
Parse + Render (48.7 KB)
| Library | ops/sec | avg time | throughput |
|---|---|---|---|
@ox-content/napi |
3738 | 0.27 ms | 177.90 MB/s |
md4x (napi) |
2270 | 0.44 ms | 108.03 MB/s |
md4w (md4c) |
1737 | 0.58 ms | 82.67 MB/s |
satteri |
1043 | 0.96 ms | 49.63 MB/s |
markdown-it |
481 | 2.08 ms | 22.88 MB/s |
marked |
327 | 3.06 ms | 15.57 MB/s |
@mizchi/markdown |
237 | 4.21 ms | 11.29 MB/s |
micromark |
29 | 34.76 ms | 1.37 MB/s |
remark |
24 | 41.92 ms | 1.14 MB/s |
On the 48.7 KB document Ox Content leads every comparison: 1.95x ahead of the
next-fastest parser (satteri) on parse-only and 1.65x ahead of md4x (napi)
on parse+render, while remaining the native core that drives the full
documentation pipeline.
Parse Only (~1 MB)
| Library | ops/sec | avg time | throughput |
|---|---|---|---|
@ox-content/napi |
144 | 6.92 ms | 147.83 MB/s |
satteri |
54 | 18.66 ms | 54.83 MB/s |
md4w (md4c) |
32 | 31.04 ms | 32.97 MB/s |
md4x (napi) |
27 | 36.37 ms | 28.13 MB/s |
markdown-it |
21 | 48.49 ms | 21.10 MB/s |
marked |
16 | 63.16 ms | 16.20 MB/s |
@mizchi/markdown |
1 | 836.85 ms | 1.22 MB/s |
remark |
1 | 1839.10 ms | 0.56 MB/s |
Parse + Render (~1 MB)
| Library | ops/sec | avg time | throughput |
|---|---|---|---|
@ox-content/napi |
164 | 6.08 ms | 168.24 MB/s |
md4x (napi) |
103 | 9.71 ms | 105.41 MB/s |
md4w (md4c) |
79 | 12.59 ms | 81.24 MB/s |
satteri |
37 | 26.70 ms | 38.32 MB/s |
markdown-it |
15 | 67.75 ms | 15.10 MB/s |
marked |
14 | 73.27 ms | 13.96 MB/s |
@mizchi/markdown |
10 | 99.77 ms | 10.25 MB/s |
micromark |
1 | 817.55 ms | 1.25 MB/s |
remark |
1 | 1981.80 ms | 0.52 MB/s |
The ~1 MB case stresses the engines at single-file-handbook scale. Ox Content
holds its lead (2.7x ahead of satteri on parse-only, 1.6x ahead of
md4x (napi) on parse+render) and sustains ~148–168 MB/s throughput, while the
incremental CST parser (@mizchi/markdown, tuned for real-time editing rather
than bulk parsing) and the unified/remark and micromark pipelines fall to
~1 op/sec — two to three orders of magnitude behind.
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.
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. The workflow posts one report with runtime 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.
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 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.