The HTML markdown-it writes, byte for byte, about 10 times faster. Rust compiled to WebAssembly.
An experiment. Can Claude Fable 5.1 port markdown-it to Rust, keep every byte, and go faster?
Demo · npm · Benchmarks · How it works
markdown-it |=== 1.00x
mdust-rs |######################### 10.14x
headline set, geometric mean, 2026-10-19
All 11,005 pages of MDN Web Docs in 120 ms. markdown-it takes 1.2 s for the same 40 MiB.
The react.dev docs in under 10 ms. 225 pages, 3.9 MiB, and markdown-it needs 82 ms.
The freeCodeCamp curriculum, 9,000 pages, in 54 ms. The fastest other wasm renderer, markdown-wasm, takes 116 ms.
Medians of 3 runs on one core, 2026-10-19, Intel Core i7-14700KF, Node 26.8.2, CORPORA=react-dev,mdn-web,fcc-curriculum RUNS=3 node bench/run.mjs. Every page rendered one after the other, same bytes out as markdown-it.
npm install mdust-rs
pnpm add mdust-rs
yarn add mdust-rs
bun add mdust-rs
deno add npm:mdust-rsEach of these was installed and rendered on 2026-10-19. Deno blocks packages younger than a day by default, so a fresh release needs --minimum-dependency-age 0 until it is older than that.
If you call markdown-it with its default preset and { html: true }, replace the call with render and every byte of your HTML stays the same.
| markdown-it | mdust-rs |
|---|---|
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt('default', { html: true });
md.render('# hi'); // '<h1>hi</h1>\n' |
import { render } from 'mdust-rs';
render('# hi'); // '<h1>hi</h1>\n' |
In Node, the module loads synchronously on import, so render works on the first line. CommonJS works too, as const { render } = require('mdust-rs').
Use it in the browser
In the browser, call await init() once before the first render. It fetches the wasm file from next to the loader.
<script type="module">
import { init, render } from './node_modules/mdust-rs/mdust_wasm.web.mjs';
await init();
document.body.innerHTML = render('# hi');
</script>Vite and webpack 5 copy the wasm file into your build. The package README covers other bundlers and every input init accepts.
Speedup over markdown-it, geometric mean, higher is faster
2026-10-19, Intel Core i7-14700KF, Linux, Node 26.8.2
headline 5 corpora used for tuning
markdown-it |=== 1.00x
markdown-wasm |=============== 6.06x
markdown-it.rs wasm |==== 1.61x
mdust-rs |######################### 10.14x
mdust native |++++++++++++++++++++++++++++++++++++++ 15.09x
holdout 6 corpora never used for tuning
markdown-it |=== 1.00x
markdown-wasm |=============== 5.95x
markdown-it.rs wasm |==== 1.55x
mdust-rs |######################### 9.85x
mdust native |+++++++++++++++++++++++++++++++++++ 13.87x
heavy 10 large corpora, 125.8 MiB in all
markdown-it |=== 1.00x
markdown-wasm |============= 5.39x
markdown-it.rs wasm |==== 1.48x
mdust-rs |######################## 9.67x
mdust native |+++++++++++++++++++++++++++++++++++ 14.16x
# npm package + build from source
Native is the same code as a Node add-on. It is not on npm. Build it from native/.
All renderers and what each set holds
Each cell is a geometric mean of markdown-it's time divided by the renderer's time. Higher is faster.
| renderer | headline | holdout | heavy |
|---|---|---|---|
| markdown-it 15.0.3 | 1.00x | 1.00x | 1.00x |
| markdown-wasm 1.2.0 | 6.06x | 5.95x | 5.39x |
| markdown-it.rs 0.6.1, wasm | 1.61x | 1.55x | 1.48x |
| mdust-rs, npm | 10.14x | 9.85x | 9.67x |
| mdust native, from source | 15.09x | 13.87x | 14.16x |
The headline set is the 5 corpora used to judge and tune speed, including the Rust book and the Node API docs. The holdout set is 6 corpora that no tuning step ever used, and mdust-rs keeps 9.85x there, so the speed is not fitted to the headline set. The heavy set is 10 large corpora with 125.8 MiB of Markdown in all, such as MDN, the Kubernetes docs and the freeCodeCamp curriculum.
How it was measured
Measured on 2026-10-19 on an Intel Core i7-14700KF, Linux, Node 26.8.2, with RUNS=7 CPU=10 node run.mjs in bench/, and with CORPORA set to the heavy corpora for that set. Each per-corpus time is the median of 7 runs pinned to one core. Per-corpus results for all 21 corpora are in the package README. node bench/chart.mjs redraws both charts in this file from bench/results.json.
40,919 cases 652 spec examples 23 corpora 0 diffs
The target is the string that new MarkdownIt('default', { html: true }).render(src) returns with markdown-it 15.0.3, and the parity check compared every output with === on 2026-10-19. On the 61 spec examples where markdown-it departs from the spec, mdust follows markdown-it.
| You get | You give up |
|---|---|
| One wasm file of 179,356 bytes and no runtime dependencies. | No options and no plugins. The output is always the default preset with { html: true }. |
A synchronous render in Node, from import or require. |
Raw HTML passes through unchanged. Sanitize untrusted input yourself. |
The same render in the browser after one await init(). |
A panic throws a WebAssembly.RuntimeError. |
One function, render(src). A string goes in and a string comes out. |
No streaming, and wasm memory never shrinks after a large document. |
| Trick | What it does |
|---|---|
| SIMD scanning | Scanners read 16 bytes per step to find line ends, the end of each run of text, and the bytes HTML escaping must replace. |
| Zero copy | Text that needs no unescaping stays a slice of your input until the renderer writes it out. |
| Bump allocator | Each render resets one allocator to the same base address instead of freeing objects one by one. |
The package README has the design notes.
Repository layout and build
Layout.
crates/mdustis the parser and renderer, ano_stdRust library.crates/mdust-wasmexportsmdust::renderto WebAssembly through two buffers that the JS loaders read and write.npm/is themdust-rsnpm package. npm refused the bare name mdust as too close to an existing package. It holds the Node loadermdust_wasm.js, its ES module wrappermdust_wasm.mjs, the browser loadermdust_wasm.web.mjs, and the types inindex.d.ts.bench/holds the parity checks and the benchmark harness, and compares every output against markdown-it.tools/gen_tables.cjsgeneratescrates/mdust/src/tables.rs, the entity and Unicode punctuation tables.crates/mdust-napiandnative/build a native Node add-on, marked private innative/package.json.logo/holds three logo concepts as SVG.
Build. You need a Rust toolchain with the wasm32-unknown-unknown target, and Node with npm.
- To build the wasm file into
npm/, runnpm --prefix npm run buildfrom the repository root. - To make the npm tarball, run
npm packinnpm/. It writesnpm/mdust-rs-0.1.1.tgz.
The build output, npm/mdust_wasm_bg.wasm and the tarball, is not committed.
Check parity and benchmark. The benchmark harness reads its corpora from bench/corpus, which is not committed.
- In
bench/, runnpm installand./fetch-corpus.shto get markdown-it and the corpora. - Run
npm run build:mdust. It buildsnpm/and copies the wasm file and the loaders intocrates/mdust-wasm/pkg, which the harness loads. - To check parity against markdown-it on every fixture suite and corpus, run
node conformance.mjs --impl mdust. - To time every implementation, run
node run.mjs.CORPORA=rust-book RUNS=1limits a run to one corpus and one sample.CPU=<n>picks the core thattasksetpins each run to. - To check the browser loader in headless Chromium, run
node browser.mjs.
run.mjs also times markdown-wasm, the markdown-it.rs crate, and a native build of mdust. To build the markdown-it.rs and native binaries, run npm run build in bench/.
MIT