CommonMark

Unreviewed Node + Browser Text
napi2.8–13× faster

CommonMark + GFM renderer powered by pulldown-cmark.

Install

pnpm add @amigo-labs/commonmark

Benchmarks

Trend (13 pts)

Benchmark

small (~0.1 KB)

8.88× vs slowest
  • @amigo-labs/commonmark renderBytesFast napi 392.47K hz · 8.88×
  • @amigo-labs/commonmark renderFast napi 390.18K hz · 8.83×
  • @amigo-labs/commonmark renderBytes napi 317.06K hz · 7.18×
  • @amigo-labs/commonmark render napi 303.34K hz · 6.87×
  • @amigo-labs/commonmark render (fast opts) napi 264.26K hz · 5.98×
  • markdown-it 59.08K hz · 1.34×
  • marked 44.18K hz

Benchmark

medium (~2.8 KB)

11.08× vs slowest
  • @amigo-labs/commonmark render (fast opts) napi 38.78K hz · 11.08×
  • @amigo-labs/commonmark renderBytes napi 32.21K hz · 9.20×
  • @amigo-labs/commonmark render napi 30.19K hz · 8.63×
  • markdown-it 5.16K hz · 1.47×
  • marked 3.50K hz

Benchmark

large (~81 KB)

10.77× vs slowest
  • @amigo-labs/commonmark render (fast opts) napi 846 hz · 10.77×
  • @amigo-labs/commonmark renderBytes napi 750 hz · 9.55×
  • @amigo-labs/commonmark render napi 376 hz · 4.79×
  • markdown-it 134 hz · 1.71×
  • marked 78.5 hz

Benchmark

batch — renderMany (500 × medium docs)

18.92× vs slowest
  • @amigo-labs/commonmark renderMany (parallel) napi 125 hz · 18.92×
  • @amigo-labs/commonmark per-call loop napi 60.8 hz · 9.21×
  • markdown-it per-call loop 9.28 hz · 1.41×
  • marked per-call loop 6.60 hz
Performance trend for CommonMark
13 commits · last 2026-05-26

README

@amigo-labs/commonmark

Blazing-fast native CommonMark + GFM renderer. Powered by Rust and pulldown-cmark, delivered as prebuilt binaries via NAPI-RS.

Not a drop-in for marked or markdown-it. This package targets the CommonMark 0.30 spec plus standard GFM extensions. Output is not byte-identical to either marked or GitHub’s renderer — see __conformance__/divergences.md.

Install

npm install @amigo-labs/commonmark

The correct prebuilt binary for your platform is selected automatically. Supported: Linux x64 (glibc+musl), Linux arm64, macOS x64, macOS arm64, Windows x64.

Usage

import { render, renderMany, Renderer } from '@amigo-labs/commonmark'

// One-shot
render('# Hello, **world**')
// → '<h1 id="hello-world">Hello, <strong>world</strong></h1>\n'

// Batch (site builder shape)
renderMany(['# Page 1', '# Page 2', '# Page 3'])
// → ['<h1 id="page-1">Page 1</h1>\n', …]

// Reusable renderer (same options across many calls)
const r = new Renderer({ gfm: true, unsafeHtml: false })
r.render('## Section')

// Buffer-input overload — skips V8 UTF-16 → UTF-8 copy on the FFI boundary.
// Measurably faster on small inputs; parity on medium/large where
// rendering dominates.
import { renderBytes } from '@amigo-labs/commonmark'
renderBytes(Buffer.from('# fast'))

Options

All options are optional.

OptionTypeDefaultDescription
gfmbooleantrueEnable GFM extensions: tables, strikethrough, task lists, autolinks.
footnotesbooleanfalseEnable footnote syntax ([^1]).
smartPunctuationbooleanfalseConvert -- → en-dash, ... → ellipsis, straight quotes → curly.
unsafeHtmlbooleanfalseAllow raw HTML blocks in Markdown. Disabled by default — raw HTML is dropped silently.
headingIdsbooleantrueAuto-generate ASCII slug IDs for headings (# Hello World<h1 id="hello-world">).

Safety

The default (unsafeHtml: false) drops raw HTML blocks and inline HTML. This is the right choice for untrusted input (user-submitted Markdown). It is not a full XSS sanitizer — link scheme filtering (javascript:, data:, etc.) is not applied. For fully-sanitized output, chain with @amigo-labs/sanitize-html:

import { render } from '@amigo-labs/commonmark'
import { sanitize } from '@amigo-labs/sanitize-html'

const unsafe_but_html_free = render(userMarkdown)
const safe = sanitize(unsafe_but_html_free)

Install for the browser

The same import works in Angular, React, Vite, esbuild, and webpack ≥ 5 — the bundler picks the WASM build via the browser conditional export:

import { render } from '@amigo-labs/commonmark'

pulldown-cmark is ~150–200 KB gzipped — under the 500 KB browser budget. The napi-only renderBytes / renderBytesFast variants are dropped from the browser build (no Buffer); renderMany runs serially in WASM (no rayon).

The XSS warning above applies doubly to browser usage — pair with @amigo-labs/sanitize-html when rendering untrusted Markdown into the DOM.

When to choose this package

  • You control the Markdown source (docs, README files, CMS authored by trusted editors) and want faster site builds.
  • Your app renders Markdown in a hot path (AI chat responses, realtime editor preview) and FFI overhead is amortized.
  • You can accept CommonMark+GFM-spec output and don’t depend on marked-specific rendering quirks.

When not to choose this package

  • You rely on marked’s tight-vs-loose list heuristics, plugin API, or raw-HTML passthrough.
  • You need markdown-it’s plugin ecosystem (anchors, containers, footnote styles, custom tokenizers).
  • You need output byte-identical to GitHub’s renderer (usernames, issue refs, SHA autolinks).

Performance

Live benchmark numbers vs marked and markdown-it are on the dashboard and in docs/data.json. Notes on the options that move them:

  • renderBytes(Buffer) avoids the V8 UTF-16 → UTF-8 copy on input — faster than render(string) on small inputs; roughly parity once rendering dominates.
  • { headingIds: false, unsafeHtml: true } enables the streaming fast-path: no event collection, no filter pass.
  • renderMany parallelises across cores via rayon for batches ≥ 8 docs where at least one doc is ≥ 512 bytes.

Conformance

pnpm test             # unit tests
pnpm test:conformance # parity + upstream + fuzz
pnpm test:all         # everything
pnpm bench            # vs marked + markdown-it

License

MIT

Perf review

Candidate review: commonmark

Status: GO (as a new package, not a drop-in for marked) · Predicted: 🟢 Green · Reviewed: 2026-04-19

Verdict

pulldown-cmark is a textbook Green shape: bytes-in / bytes-out, substantial compute work per byte, no object traversal, no callback-boundary problem. As a new package with honest positioning (CommonMark + GFM spec-strict, not marked-compatible) it sidesteps the parity trap that blocks marked itself.

JS package

  • npm: no direct candidate as a drop-in target — this package is a new product. Comparison alternatives in JS: marked (~30M/week), markdown-it (~25M/week), commonmark.js (~2M/week)
  • Downloads: n/a (newcomer)
  • Exports / API surface: kept small — render(md: string, opts?): string, possibly parse(md) → token-array for streaming/walk use-cases
  • Typical input: Markdown document 1 KB – 1 MB
  • Typical output: HTML string
  • Realistic median use-case: site builders (Astro/Docusaurus-style tools) rendering 500–5000 docs per build; CLI README viewers; AI chat UIs that render Markdown responses server-side

Rust replacement

  • Candidate crate(s): pulldown-cmark (primary — minimal, fast, CommonMark-compliant, GFM extensions via feature flags), comrak (more feature-rich, more GFM parity with GitHub, larger bundle)
  • Maintenance / license: pulldown-cmark active (raphlinus + contributors), MIT; comrak active, BSD-2
  • Known gotchas / divergences: CommonMark 0.30 spec as the baseline — if we communicate that cleanly, “divergence” is not a bug but a feature

BACKLOG check

No existing BACKLOG entry. marked is listed there as a drop-in NO-GO — this package is explicitly not a marked replacement but a stand-alone offering.

FFI-overhead prediction

FactorAssessment
Per-call algorithmic workSubstantial: 100 KB Markdown ~500 µs – 1 ms in pulldown-cmark, JS baseline marked ~5 ms → 5–10× headroom
Input size distribution1 KB – 1 MB, Buffer input possible → FFI input cost negligible
Output size distributionHTML string ~1.5× input size; 0.35 ns/byte FFI output cost = ~50 µs at 150 KB output — tolerable
Reusable setup (stateful potential)Low — options are small, no expensive setup
Batch-usage realismHigh: site builders render hundreds of docs per build; a renderMany(docs: string[]) API makes sense
FFI-share estimate vs. Rust work<15% on documents ≥10 KB; ~40% at 1 KB but speedup is still ≥2×

Classification reasoning

The shape matches sanitize-html and inflate in the repo exactly: bytes-in, substantial compute, bytes-out. Not a deep-equal shape (no object traversal), not a handlebars shape (no callbacks), not a mime shape (no FFI trap). pulldown-cmark is also a pull parser, streams internally — memory footprint is good.

The single condition for Green instead of Yellow: the smallest realistic input has to perform cleanly. At 1 KB Markdown JS marked is ~50 µs, pulldown-cmark over FFI comes in at an estimated ~15–20 µs — above 2×. At 100 KB it becomes 8–10×. The 2×-at-smallest-input gate holds.

GFM parity with pulldown-cmark is good: tables, strikethrough, task lists, footnotes, autolinks via feature flags. It’s not marked-compatible, but it is spec-compatible — and a spec-compatible CommonMark/GFM is an honest, defensible position.

If GO — proposed port

  • Recommended crate name: @amigo-labs/commonmark

  • Primary API sketch:

    export interface CommonMarkOptions {
      gfm?: boolean;                // default true (tables, strike, task-lists, autolinks)
      footnotes?: boolean;          // default false
      smartPunctuation?: boolean;   // default false
      unsafeHtml?: boolean;         // default false — filter raw HTML
      headingIds?: boolean;         // default true — slugify headings
    }
    
    export function render(markdown: string | Buffer, opts?: CommonMarkOptions): string;
    
    // Batch API for site builders
    export function renderMany(docs: Array<string | Buffer>, opts?: CommonMarkOptions): string[];
    
    // Optional: stateful renderer class for repeated calls with the same opts set
    export class Renderer {
      constructor(opts?: CommonMarkOptions);
      render(markdown: string | Buffer): string;
    }
    
  • Must-have benchmark scenarios:

    • small: 1 KB Markdown (typical blog paragraph) vs. marked, markdown-it
    • medium: 50 KB (long blog post / README) vs. same
    • large: 500 KB (Docusaurus API reference) vs. same
    • batch: renderMany(500 × 10KB docs) — site-build shape
    • realistic median: AI chat response shape, 2–5 KB with code blocks + inline formatting
  • Acceptance thresholds (Green gate):

    • ≥2× vs. marked at 1 KB
    • ≥5× at 50 KB
    • ≥8× at 500 KB
    • renderMany per-item overhead ≤15% vs. single call (otherwise no batch gain)
  • Risks:

    • Feature-request drift: users want marked plugins or markdown-it plugins ported — clearly document “spec-only, no plugin API in v1”
    • Heading IDs / slug behavior: github-slugger is the de-facto standard in JS; we either have to reuse slug/slugify (we ship @amigo-labs/slugify) or introduce a new headingSlugger
    • HTML sanitizing interaction: unsafeHtml: false must be clearly documented; users who need raw HTML will turn it on and then have an XSS incident → README warning, link to @amigo-labs/sanitize-html as the recommended chain
    • GFM edge cases vs. GitHub: pulldown-cmark’s GFM ≈ GitHub’s GFM, but not byte-identical. Irrelevant for most users, problematic for GitHub-rendering clones — document in the README

If NO-GO — BACKLOG entry

n/a — recommendation is GO.