Skip to content

Performance

Benchmark Results

All numbers are measured on Apple M-series hardware (warm singletons, all caches populated). Run bun bench to reproduce on your machine.

Throughput by document type — PNG, scale 2 (default)

Document typemeanp50p90
Plain text (prose + lists + blockquotes)116 ms115 ms120 ms
Code-heavy (3 languages, shiki)101 ms101 ms104 ms
Math-heavy (4 block + 3 inline formulas)100 ms99 ms103 ms
Mixed (math + code)98 ms97 ms100 ms

Output format — mixed document

Formatmeanp50p90note
SVG5.6 ms5.6 ms6.5 mslayout + serialize only
PNG scale 299 ms98 ms102 msfull rasterize + encode

SVG is ~17× faster than PNG because it skips rasterisation entirely.

PNG scale factor — mixed document

scaleResolutionmean
11080 × ~650 (0.7 MP)29 ms
1.51620 × ~975 (1.6 MP)58 ms
2 (default)2160 × ~1300 (2.8 MP)99 ms
33240 × ~1950 (6.3 MP)214 ms

Concurrency — plain document, PNG scale 2

Modemean per batch
1× sequential118 ms
2× parallel127 ms
4× parallel192 ms
8× parallel363 ms

Where the time goes

The rendering pipeline has five stages. Their relative cost for a typical PNG render:

StageApprox. timeNotes
Markdown parse + model build< 1 msmicromark + mdast
Syntax highlighting (shiki)< 1 mslazy singleton, cached per language
Math pre-render (MathJax)< 2 mslazy singleton, SVG cached per formula
Layout + pagination2–4 msblock engine, line-breaking, pagination
Canvas draw (skia-canvas)1–2 mspaint operations
PNG encode (toBuffer)~93 mspure CPU PNG compression — ~94% of total

canvas.toBuffer('png') dominates. Everything else — parsing, Shiki, MathJax, layout, and drawing — totals less than 8 ms per page. PNG compression scales linearly with pixel count (proportional to scale²).

Implications

  • Use SVG for previews: at 5–6 ms per page, SVG gives near-instant output for pipelines that do not need raster images.
  • Use scale: 1 for batch drafts: cuts encode time from ~99 ms to ~29 ms while producing full-resolution layout.
  • Use scale: 2 (default) for final output: retina quality at ~100 ms/page.
  • Parallel renders scale well: 4 simultaneous renders complete in ~192 ms (vs ~470 ms sequential) on an 8-core M-series chip.

Cold-start overhead

On first render after process start, two lazy singletons must initialise:

SingletonCold-start costTrigger
MathJax~180 msFirst render containing $ or $$ math
Shiki~80 msFirst render containing a fenced code block

Both singletons are initialised at most once per process. Subsequent renders pay no initialisation cost.

If your application renders many documents, a single warm-up call at startup eliminates cold-start latency for all user-facing renders:

ts
import { renderMarkdown } from 'marknative'

// Call once at startup to initialise all lazy singletons
await renderMarkdown('# Warm-up\n\n```ts\nconst x = 1\n```\n\n$E = mc^2$')

// All subsequent renders start immediately

Tuning recommendations

Use caseRecommended settings
Fast preview (draft quality)format: 'svg' or scale: 1
Standard output (retina)scale: 2 (default)
Print / high-DPI exportscale: 3
Batch processing (throughput)Promise.all up to CPU core count
Single-image output (long doc)singlePage: true, note height capped at 16 384 px

Released under the MIT License.