Skip to content

Performance ​

Where the three engine implementations stand on raw HTML-rendering throughput, and how they compare to their Markdown / CommonMark / Djot counterparts in the same language.

This page is about speed. For correctness and conformance across the implementations, see Implementation Comparison, which is a different axis (corpus pass rate, cross-implementation diffs, extension-hook surface).

NOTE

Throughput numbers are machine- and load-dependent. Absolute MB/s drift between runs on a loaded machine; the ratios between engines on the same run are the stable signal. Treat the tables as "carve is roughly X% of engine Y", not as a fixed MB/s guarantee.

Methodology ​

  • Input: a ~48 KB document of core syntax (headings, nested lists, **bold** / *italic* / `code`, links, a GFM table, a blockquote, a fenced code block, reference links) repeated to size. The same logical content is used for every language so the comparison is apples-to-apples.
  • Measurement: in-process, warm (discard the first iterations), then min-of-5-trials of an N-iteration loop. Minimum (not mean) is used to reject scheduler noise on a loaded machine.
  • Metric: MB/s = input_bytes / wall_time.
  • carve config: core conversion (no extensions). Extensions add a small, measurable overhead.
  • The benchmark harnesses themselves are kept out of this repo (they pull in every competing engine as a dependency). Each implementation ships its own regression guard instead, listed under Regression guards. For cross-engine render-speed comparisons (carve-js / carve-php / carve-rs), see carve-bench.

Snapshot (2026-06-22) ​

Each carve engine against the Djot and CommonMark engine in the same language.

LanguagecarveDjotCommonMark
Rustcarve-rs 24.3jotdown 55.7comrak 58.7 / pulldown-cmark 181.4
JScarve-js 4.5djot.js 9.1markdown-it 19.6
PHPcarve-php 1.08djot-php 1.48league/commonmark (GFM) 0.68

MB/s, ~48 KB input, min-of-5, same machine (variable load).

Ratios on this run:

  • carve-rs = ~44% of jotdown, ~13% of pulldown-cmark.
  • carve-js = ~49% of djot.js, ~23% of markdown-it.
  • carve-php = ~73% of djot-php, and ~1.6x league/commonmark (the one pairing where a carve engine is faster than the same-language CommonMark engine, because league/commonmark is itself comparatively heavy).

Why carve trails the fastest engines ​

carve builds a full owned AST (parse to a node tree, optionally transform it, then render), which is the architecture that makes the security defenses and the extension contract clean to implement. The fastest reference engines take a different shape:

  • jotdown and pulldown-cmark are near-zero-allocation event / pull parsers: they stream tokens straight to the renderer and never materialize a tree. That is structurally cheaper, and it is why pulldown-cmark is ~7x carve-rs.
  • carve's parse phase allocates one owned String per text run, attribute, and identifier, plus a Vec per container node. That allocation count, not copy volume, is the dominant cost once the render hot path is tuned.

The default path therefore remains more expensive than a pull parser. carve-rs and carve-js also expose a conservative streaming boundary (try_render_html_streaming / tryRenderHtmlStreaming) for render-only callers. It reports when a document needs the full AST and falls back without emitting partial output. The initial API returns one completed HTML chunk, so it defines the safe fast-path boundary but does not yet improve time to first byte.

Regression guards ​

Each implementation guards against perf regressions in its own test suite (thresholds, not recorded MB/s):

  • carve-rs: tests/perf_regressions.rs
  • carve-js: test/inline-position-perf.test.ts, test/table-rowspan-perf.test.ts
  • carve-php: tests/benchmark/ (a runnable cross-language harness with its own README.md; produces numbers on demand rather than a checked-in snapshot)

Released under the MIT License.